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
?
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
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:
(Factory.build :company).attributes.symbolize_keys
, ce qui crée de nouveaux problèmes.Date.inspect
n'indique pas les millisecondes.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.
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.
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.
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