web-dev-qa-db-fra.com

Comment compléter le test du contrôleur rspec put depuis un échafaudage

J'utilise un échafaudage pour générer des tests de contrôleur rspec. Par défaut, le test est créé comme suit:

  let(:valid_attributes) {
    skip("Add a hash of attributes valid for your model")
  }

  describe "PUT update" do
    describe "with valid params" do
      let(:new_attributes) {
        skip("Add a hash of attributes valid for your model")
      }

      it "updates the requested doctor" do
        company = Company.create! valid_attributes
        put :update, {:id => company.to_param, :company => new_attributes}, valid_session
        company.reload
        skip("Add assertions for updated state")
      end

En utilisant FactoryGirl, j'ai complété ceci avec:

  let(:valid_attributes) { FactoryGirl.build(:company).attributes.symbolize_keys }

  describe "PUT update" do
    describe "with valid params" do
      let(:new_attributes) { FactoryGirl.build(:company, name: 'New Name').attributes.symbolize_keys }

      it "updates the requested company", focus: true do
        company = Company.create! valid_attributes
        put :update, {:id => company.to_param, :company => new_attributes}, valid_session
        company.reload
        expect(assigns(:company).attributes.symbolize_keys[:name]).to eq(new_attributes[:name])

Cela fonctionne, mais il semble que je devrais être capable de tester tous les attributs, au lieu de simplement tester le nom modifié. J'ai essayé de changer la dernière ligne en:

class Hash
  def delete_mutable_attributes
    self.delete_if { |k, v| %w[id created_at updated_at].member?(k) }
  end
end

  expect(assigns(:company).attributes.delete_mutable_attributes.symbolize_keys).to eq(new_attributes)

Cela a presque fonctionné, mais je reçois l'erreur suivante de rspec concernant les champs BigDecimal:

   -:latitude => #<BigDecimal:7fe376b430c8,'0.8137713195 830835E2',27(27)>,
   -:longitude => #<BigDecimal:7fe376b43078,'-0.1270954650 1027958E3',27(27)>,
   +:latitude => #<BigDecimal:7fe3767eadb8,'0.8137713195 830835E2',27(27)>,
   +:longitude => #<BigDecimal:7fe3767ead40,'-0.1270954650 1027958E3',27(27)>,

Utiliser rspec, factory_girl et scaffolding est incroyablement courant. Mes questions sont donc les suivantes:

Quel est un bon exemple de test rspec et factory_girl pour une mise à jour PUT avec des paramètres valides? Est-il nécessaire d’utiliser attributes.symbolize_keys et de supprimer les clés mutables? Comment puis-je obtenir ces objets BigDecimal à évaluer comme eq?

26
dankohn

Ok, c’est ce que je fais. Je ne prétends pas suivre strictement les meilleures pratiques, mais je me concentre sur la précision de mes tests, la clarté de mon code et l’exécution rapide de ma suite.

Alors prenons l'exemple d'une UserController

1- Je n'utilise pas FactoryGirl pour définir les attributs à publier sur mon contrôleur, car je souhaite garder le contrôle de ces attributs. FactoryGirl est utile pour créer un enregistrement, mais vous devez toujours définir manuellement les données impliquées dans l’opération que vous testez. C’est mieux pour la lisibilité et la cohérence.

À cet égard, nous allons définir manuellement les attributs publiés

let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }

2- Ensuite, je définis les attributs que j'attends pour l'enregistrement mis à jour. Il peut s'agir d'une copie exacte des attributs postés, mais il se peut que le contrôleur fasse un travail supplémentaire et que nous souhaitons également le tester. Supposons donc que, une fois que notre utilisateur ait mis à jour ses informations personnelles, notre contrôleur ajoute automatiquement un drapeau need_admin_validation.

let(:expected_update_attributes) { valid_update_attributes.merge(need_admin_validation: true) }

C'est également à cet endroit que vous pouvez ajouter une assertion pour un attribut qui doit rester inchangé. Exemple avec le champ age, mais cela peut être n'importe quoi

let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }

3- Je définis l'action, dans un bloc let. Avec les 2 let précédents, je trouve que cela rend mes spécifications très lisibles. Et il est également facile d'écrire shared_examples

let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }

4- (à partir de ce moment, tout est dans l'exemple partagé et les correspondeurs rspec personnalisés dans mes projets) Il est temps de créer l'enregistrement original, pour cela nous pouvons utiliser FactoryGirl.

let!(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }

Comme vous pouvez le constater, nous définissons manuellement la valeur age car nous souhaitons vérifier qu’elle n’a pas changé au cours de l’action update. En outre, même si l’usine a déjà fixé l’âge à 25 ans, je l’écrase toujours pour que mon test ne se rompt pas si je change d’usine.

Deuxième chose à noter: ici, nous utilisons let! avec un bang. En effet, il peut arriver que vous souhaitiez tester l'action d'échec de votre contrôleur, et le meilleur moyen de le faire est de stub valid? et de renvoyer false. Une fois que vous avez stub valid? vous ne pouvez plus créer d'enregistrements pour la même classe, let! avec un bang créerait l'enregistrement avant le stub de valid?

5- Les assertions elles-mêmes (et enfin la réponse à votre question) 

before { action }
it {
  assert_record_values record.reload, expected_update_attributes
  is_expected.to redirect_to(record)
  expect(controller.notice).to eq('User was successfully updated.')
}

Résumer Donc, en ajoutant tout ce qui précède, voici à quoi ressemble la spécification

describe 'PATCH update' do
  let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }
  let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
  let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
  let(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
  before { action }
  it {
    assert_record_values record.reload, expected_update_attributes
    is_expected.to redirect_to(record)
    expect(controller.notice).to eq('User was successfully updated.')
  }
end

assert_record_values est l’assistant qui simplifiera votre rspec.

def assert_record_values(record, values)
  values.each do |field, value|
    record_value = record.send field
    record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String)

    expect(record_value).to eq(value)
  end
end

Comme vous pouvez le constater avec cette aide simple lorsque nous attendons une BigDecimal, nous pouvons simplement écrire ce qui suit, et cette aide fait le reste

let(:expected_update_attributes) { {latitude: '0.8137713195'} }

Donc, à la fin, et pour conclure, lorsque vous aurez écrit vos shared_examples, vos helpers et vos correspondants personnalisés, vous pourrez conserver vos spécifications à un niveau extrêmement sec. Dès que vous commencez à répéter la même chose dans les spécifications de votre contrôleur, trouvez comment vous pouvez le reformuler. Cela peut prendre du temps au début, mais quand c'est fait, vous pouvez écrire les tests pour un contrôleur entier en quelques minutes.


Et un dernier mot (je ne peux pas arrêter, j'aime Rspec), voici à quoi ressemble mon aide. Il est utilisable pour n'importe quoi en fait, pas seulement pour les modèles.

def assert_records_values(records, values)
  expect(records.length).to eq(values.count), "Expected <#{values.count}> number of records, got <#{records.count}>\n\nRecords:\n#{records.to_a}"
  records.each_with_index do |record, index|
    assert_record_values record, values[index], index: index
  end
end

def assert_record_values(record, values, index: nil)
  values.each do |field, value|
    record_value = [field].flatten.inject(record) { |object, method| object.try :send, method }
    record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String)

    expect_string_or_regexp record_value, value,
                            "#{"(index #{index}) " if index}<#{field}> value expected to be <#{value.inspect}>. Got <#{record_value.inspect}>"
  end
end

def expect_string_or_regexp(value, expected, message = nil)
  if expected.is_a? String
    expect(value).to eq(expected), message
  else
    expect(value).to match(expected), message
  end
end
30
Benj

Ceci est la publication de l'interrogateur. J'ai du mal à comprendre un grand nombre de problèmes qui se chevauchent, alors je voulais simplement vous faire part de la solution que j'ai trouvée.

tldr; C'est trop compliqué d'essayer de confirmer que tous les attributs importants reviennent inchangés d'un PUT. Il suffit de vérifier que l'attribut modifié correspond à vos attentes.

Les problèmes que j'ai rencontrés:

  1. FactoryGirl.attributes_for ne renvoie pas toutes les valeurs, donc FactoryGirl: attributs_pour ne me donner pas les attributs associés suggère d'utiliser (Factory.build :company).attributes.symbolize_keys, ce qui crée de nouveaux problèmes.
  2. Plus précisément, les énumérations de Rails 4.1 sont affichées sous forme d’entiers au lieu de valeurs d’énumération, comme indiqué ici: https://github.com/thoughtbot/factory_girl/issues/680
  3. Il s’avère que le problème BigDecimal était un problème redoutable, causé par un bogue dans le matcher rspec qui produisait des différences incorrectes. Ceci a été établi ici: https://github.com/rspec/rspec-core/issues/1649
  4. L'échec de l'assistant est dû à des valeurs de date qui ne correspondent pas. Cela est dû au fait que l'heure renvoyée est différente, mais cela ne s'affiche pas, car Date.inspect n'indique pas les millisecondes.
  5. J'ai contourné ces problèmes avec une méthode de hachage patché par un singe qui symbolise les valeurs de clés et de chaînes.

Voici la méthode de hachage, qui pourrait aller dans Rails_spec.rb:

class Hash
  def symbolize_and_stringify
    Hash[
      self
      .delete_if { |k, v| %w[id created_at updated_at].member?(k) }
      .map { |k, v| [k.to_sym, v.to_s] }
    ]
  end
end

Alternativement (et peut-être de préférence), j'aurais pu écrire un matcher rspec personnalisé, qui parcourt chaque attribut et compare leurs valeurs individuellement, ce qui aurait permis de contourner le problème de la date. C'était l'approche de la méthode assert_records_values au bas de la réponse que j'ai sélectionnée par @Benjamin_Sinclaire (pour laquelle, merci).

Cependant, j'ai plutôt décidé de revenir à l'approche beaucoup plus simple consistant à coller avec attributes_for et à comparer simplement l'attribut que j'ai modifié. Plus précisément:

  let(:valid_attributes) { FactoryGirl.attributes_for(:company) }
  let(:valid_session) { {} }

  describe "PUT update" do
    describe "with valid params" do
      let(:new_attributes) { FactoryGirl.attributes_for(:company, name: 'New Name') }

      it "updates the requested company" do
        company = Company.create! valid_attributes
        put :update, {:id => company.to_param, :company => new_attributes}, valid_session
        company.reload
        expect(assigns(:company).attributes['name']).to match(new_attributes[:name])
      end

J'espère que cet article permettra aux autres d'éviter de répéter mes enquêtes.

5
dankohn

Eh bien, j’ai fait quelque chose de plus simple, j’utilise Fabricator, mais je suis à peu près sûr que c’est la même chose avec FactoryGirl:

  let(:new_attributes) ( { "phone" => 87276251 } )

  it "updates the requested patient" do
    patient = Fabricate :patient
    put :update, id: patient.to_param, patient: new_attributes
    patient.reload
    # skip("Add assertions for updated state")
    expect(patient.attributes).to include( { "phone" => 87276251 } )
  end

De plus, je ne suis pas sûr de savoir pourquoi vous construisez une nouvelle usine, le verbe PUT est supposé ajouter de nouvelles choses, non? Et ce que vous testez si ce que vous avez ajouté en premier lieu (new_attributes), existe après la put du même modèle.

Ce code peut être utilisé pour résoudre vos deux problèmes:

it "updates the requested patient" do
  patient = Patient.create! valid_attributes
  patient_before = JSON.parse(patient.to_json).symbolize_keys
  put :update, { :id => patient.to_param, :patient => new_attributes }, valid_session
  patient.reload
  patient_after = JSON.parse(patient.to_json).symbolize_keys
  patient_after.delete(:updated_at)
  patient_after.keys.each do |attribute_name|
    if new_attributes.keys.include? attribute_name
      # expect updated attributes to have changed:
      expect(patient_after[attribute_name]).to eq new_attributes[attribute_name].to_s
    else
      # expect non-updated attributes to not have changed:
      expect(patient_after[attribute_name]).to eq patient_before[attribute_name]
    end
  end
end

Il résout le problème de la comparaison des nombres à virgule flottante en convertissant les valeurs en une représentation sous forme de chaîne à l'aide de JSON.

Cela résout également le problème de vérifier que les nouvelles valeurs ont été mises à jour mais que les autres attributs n'ont pas changé.

D'après mon expérience, cependant, à mesure que la complexité augmente, la chose habituelle consiste à vérifier un état d'objet spécifique au lieu de "s'attendre à ce que les attributs que je ne mets pas à jour ne changent pas". Imaginez, par exemple, que d'autres attributs changent lors de la mise à jour dans le contrôleur, tels que "éléments restants", "certains attributs d'état" ... Vous souhaitez vérifier les modifications spécifiques attendues, qui peuvent être supérieures aux modifications mises à jour. les attributs.

2
chipairon

Voici ma façon de tester PUT. C’est un extrait de mon notes_controller_spec, l’idée principale devrait être claire (dites-moi si ce n’est pas le cas):

RSpec.describe NotesController, :type => :controller do
  let(:note) { FactoryGirl.create(:note) }
  let(:valid_note_params) { FactoryGirl.attributes_for(:note) }
  let(:request_params) { {} }

  ...

  describe "PUT 'update'" do
    subject { put 'update', request_params }

    before(:each) { request_params[:id] = note.id }

    context 'with valid note params' do
      before(:each) { request_params[:note] = valid_note_params }

      it 'updates the note in database' do
        expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)
      end
    end
  end
end

Au lieu de FactoryGirl.build(:company).attributes.symbolize_keys, j'écrirais FactoryGirl.attributes_for(:company). Il est plus court et ne contient que les paramètres que vous avez spécifiés dans votre usine.


Malheureusement, c'est tout ce que je peux dire sur vos questions.


P.S. Cependant, si vous établissez une vérification d’égalité BigDecimal sur la couche de base de données en écrivant dans le style suivant:

expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)

cela peut fonctionner pour vous.

1
nsave

Test de l'application Rails avec rspec-Rails gem . Création de l'échafaudage de l'utilisateur . Vous devez maintenant transmettre tous les exemples du fichier user_controller_spec.rb.

Cela a déjà été écrit par le générateur d'échafaudage. Il suffit de mettre en œuvre

let(:valid_attributes){ hash_of_your_attributes} .. like below
let(:valid_attributes) {{ first_name: "Virender", last_name: "Sehwag", gender: "Male"}
  } 

Passons maintenant de nombreux exemples de ce fichier.

Pour invalid_attributes, assurez-vous d’ajouter les validations sur l’un des champs et 

let(:invalid_attributes) {{first_name: "br"}
  }

Dans le modèle des utilisateurs, la validation de prenom est as => 

  validates :first_name, length: {minimum: 5}, allow_blank: true

Maintenant, tous les exemples créés par les générateurs vont passer pour cet controller_spec

0