Comment effectuer le test rspec put controller de scaffold

j'utilise un échafaudage pour générer des tests de contrôle rspec. Par défaut, il crée le test comme:

  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 rempli 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 en mesure de tester tous les attributs, au lieu de simplement tester le nom modifié. J'ai essayé de changer la dernière ligne:

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)

ça a presque marché, mais j'obtiens l'erreur suivante de rspec

   -: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 échafaudages est incroyablement commun, donc mes questions sont:

Quel est un bon exemple de test rspec et factory_girl pour une mise à jour PUT avec params valide? 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 ?

24
demandé sur dankohn 2014-07-10 21:21:53

6 réponses

Ok donc c'est comme ça que je fais, Je ne fais pas semblant de 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 un exemple de UserController

1- Je n'utilise pas FactoryGirl pour définir les attributs à poster à mon controller, parce que je veux garder le contrôle de ces attributs. FactoryGirl est utile pour créer un enregistrement, mais vous si vous définissez 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 affichés

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

2- puis je définis les attributs que j'attends pour l'enregistrement mis à jour, il peut être une copie exacte des attributs affichés, mais il peut être que le contrôleur faire un peu de travail supplémentaire et nous voulons également tester cela. Alors disons pour notre exemple: une fois que notre Utilisateur a mis à jour ses informations personnelles, notre contrôleur ajoute automatiquement un need_admin_validation drapeau

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

C'est aussi là que vous pouvez ajouter une assertion pour un attribut qui doit rester inchangé. Exemple avec le champ age , mais il 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 le précédent 2 let je trouve qu'il fait mes spécifications très lisible. Et il rend également facile d'écrire shared_examples

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

4- (à partir de ce point tout est dans l'exemple partagé et RSpec personnalisé matchers dans mes projets) le temps de créer le disque original, pour que nous pouvons utiliser FactoryGirl

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

comme vous pouvez le voir, nous avons défini manuellement la valeur de age car nous voulons vérifier qu'elle n'a pas changé pendant l'action update . Aussi, même si l'usine j'ai déjà fixé l'âge à 25 ans, je l'écrase toujours pour que mon test ne se casse pas si je change d'usine.

Deuxième chose à noter: ici, nous utilisons let! avec un bang. C'est parce que parfois vous pouvez vouloir tester l'action de défaillance de votre contrôleur, et la meilleure façon de le faire est de couper valid? et de retourner false. Une fois que vous appuyez sur valid? vous ne pouvez plus créer des enregistrements pour la même classe, let! avec un bang créerait l'enregistrement avant le talon de valid?

5- Les assertions lui-même (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ésumé 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 va rendre votre rspece plus simple.

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 peut voir avec ce helper simple quand nous nous attendons à un BigDecimal , nous pouvons juste écrire ce qui suit, et le helper faire le reste

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

ainsi, à la fin, et pour conclure, lorsque vous avez écrit vos exemples shared_ex, helpers, and custom matchers, vous pouvez garder vos spécifications super DRY. Dès que vous commencez à répéter toujours la même chose dans vos contrôleurs spécifications de trouver comment vous pouvez restructurer. Cela peut prendre du temps au début, mais quand c'est fait, vous pouvez écrivez les tests pour un contrôleur entier en quelques minutes


et un dernier mot (Je ne peux pas m'arrêter, J'aime Rspec) voici à quoi ressemble mon aide complète. Il est utilisable pour n'importe quoi en fait, pas seulement 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
28
répondu Benj 2014-07-26 00:28:42

c'est le message de l'auteur de la question. J'ai dû aller un peu dans le terrier du lapin pour comprendre les multiples problèmes qui se chevauchent ici, donc je voulais juste faire un rapport sur la solution que j'ai trouvée.

tldr; il est trop difficile d'essayer de confirmer que chaque attribut important revient inchangé d'un PUT. Il suffit de vérifier que l'attribut modifié est ce que vous attendez.

Les problèmes que j'ai rencontré:

  1. FactoryGirl.attributes_for ne renvoie pas toutes les valeurs, donc FactoryGirl: attributes_for ne me donne pas les attributs associés suggère d'utiliser (Factory.build :company).attributes.symbolize_keys , ce qui finit par créer de nouveaux problèmes.
  2. spécifiquement, les Rails 4.1 enums montrent comme des entiers au lieu des valeurs enum, comme rapporté ici: https://github.com/thoughtbot/factory_girl/issues/680
  3. il s'avère que le BigDecimal question était un rouge hareng, causé par un bug dans le matcher rspec qui produit des diffs incorrects. Ceci a été établi ici: https://github.com/rspec/rspec-core/issues/1649
  4. la défaillance réelle de l'apparieur est causée par des valeurs de Date qui ne correspondent pas. Cela est dû au fait que le temps retourné est différent, mais il ne s'affiche pas parce que Date.inspect ne montre pas millisecondes.
  5. j'ai contourné ces problèmes avec une méthode de hachage patché de singe qui symbolise les clés et les valeurs stringifs.

Voici la méthode Hash, 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 RSpec personnalisé que itère à travers chaque attribut et compare leurs valeurs individuellement, ce qui aurait fonctionné autour de la question de date. C'était l'approche de la méthode assert_records_values au bas de la réponse que j'ai choisie par @Benjamin_Sinclaire (pour qui, je vous remercie).

cependant, j'ai décidé à la place de revenir à l'approche beaucoup, beaucoup plus simple de rester avec attributes_for et juste en comparant l'attribut que j'ai changé. 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 ce billet permettra aux autres de ne pas répéter mes recherches.

5
répondu dankohn 2017-05-23 11:55:00

Eh bien, j'ai fait quelque chose de plus simple, J'utilise un manufacturier, mais je suis assez 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

aussi, je ne suis pas sûr pourquoi vous construisez une nouvelle usine, mettre verbe est censé ajouter de nouveaux trucs, Non?. Et ce que vous testez si ce que vous avez ajouté en premier lieu ( new_attributes ), se trouve exister après le put dans le même modèle.

3
répondu Pablo Olmos de Aguilera C. 2014-07-11 21:13:11

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 représentation de chaîne de caractères à L'aide de JSON.

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

Dans mon expérience, cependant, que la complexité augmente, la chose à faire est de vérifier certains l'état de l'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 au fur et à mesure que la mise à jour est effectuée dans le contrôleur, comme "éléments restants", "certains attributs de statut"... Vous souhaitez vérifier les changements spécifiques attendus, qui peuvent être plus que les attributs mis à jour.

2
répondu chipairon 2014-07-19 17:38:12

voici ma façon de tester mis. C'est un extrait de mon notes_controller_spec , l'idée principale doit être claire (dites-moi si non):

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.


Mais si vous posez BigDecimal equality vérifiez sur la couche de base de données en écrivant dans le style comme

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

ça peut marcher pour vous.

1
répondu nsave 2014-07-18 15:07:40

Test de l'application rails avec rspec-gem rails. Créé à l'échafaudage de l'utilisateur. Vous devez maintenant passer tous les exemples de user_controller_spec.rb

cela a déjà été écrit par le générateur d'échafaudages. 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"}
  } 

va maintenant passer 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 les utilisateurs du modèle .. la validation pour first_name est as = >

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

maintenant tous les exemples créés par les générateurs passeront pour ce controller_spec

0
répondu Navnath Shendage 2017-05-03 15:24:34