RSpec est un excellent outil dans le processus de développement BDD utilisé pour écrire des tests lisibles, et vérifier le bon déroulement de la conception de votre application.
Il existe de nombreuses ressources en ligne vous donnant un aperçu de ce que vous pouvez faire avec RSpec, mais peu d'entre elles vous disent comment améliorer votre suite de tests.
Better Specs essaye de remplir ce rôle en collectant les bonnes pratiques que les autres développeurs ont acquis au cours de leurs années d'expérience.
Comment décrire vos méthodes
Soyez clair dans la description de vos méthodes. Par exemple, utiliser la convention issue de la documentation Ruby qui consiste à utiliser .
(ou ::
) lorsque vous faites référence à une méthode de classe, et #
lorsque vous faites référence à une méthode d'instance.
mauvais
describe 'the authenticate method for User' do
describe 'if the user is an admin' do
bon
describe '.authenticate' do
describe '#admin?' do
Utiliser «context»
«Context» est une méthode qui permet d'obtenir des tests clairs, et une bonne organisation. Sur le long terme, elle devrait vous permettre de garder des tests lisibles.
mauvais
it 'has 200 status code if logged in' do
expect(response).to respond_with 200
end
it 'has 401 status code if not logged in' do
expect(response).to respond_with 401
end
bon
context 'when logged in' do
it { is_expected.to respond_with 200 }
end
context 'when logged out' do
it { is_expected.to respond_with 401 }
end
Quand vous utilisez «context», commencer votre description avec «when» ou avec «with».
Garder vos descriptions courtes
Vos descriptions ne doivent jamais dépasser 40 caractères. Si c'est le cas, découpez les en utilisant «context».
mauvais
it 'has 422 status code if an unexpected params will be added' do
bon
context 'when not valid' do
it { should respond_with 422 }
end
Dans cet exemple, nous avons supprimé la description par it { should respond_with 422 }
. En lançant ce test, vous obtenez une réponse lisible.
Sortie formatée
when not valid
it should respond with 422
Test d'attente unitaire
Une seule affirmation par test. Ce qui vous permet de détecter le test qui a échoué, et également de déceler les erreurs possibles. Ce conseil contribue à rendre votre code plus lisible.
Dans une spécification unitaire isolée, spécifier un seul comportement. Des attentes multiples dans un même exemple sont le signe que vous devriez spécifier plusieurs comportements.
Dans le cas de tests non isolés (intégration avec une base de donnée, un service web externe), vous noterez une baisse de performance en assignant un comportement différent pour chaque test. Dans ce cas uniquement spécifier plus d'un comportement par test.
bon (isolé)
it { should respond_with_content_type(:json) }
it { should assign_to(:resource) }
bon (non isolé)
it 'creates a resource' do
expect(response).to respond_with_content_type(:json)
expect(response).to assign_to(:resource)
end
Test de tous les cas possibles
Tester est une bonne pratique, mais est inutile si vous ne testez pas tous les cas possibles. Par exemple, dans l'exemple suivant.
Destroy action
before_filter :find_owned_resources
before_filter :find_resource
def destroy
render 'show'
@consumption.destroy
end
L'erreur que je vois souvent est que l'on teste seulement si la ressource a été supprimée. Mais il y a au moins deux autres cas : quand la ressource n'est pas trouvée et quand vous n'êtes pas le propriétaire de la ressource. En règle générale, penser à toutes les entrées possibles, et tester les.
mauvais
it 'shows the resource'
bon
describe '#destroy' do
context 'when resource is found' do
it 'responds with 200'
it 'shows the resource'
end
context 'when resource is not found' do
it 'responds with 404'
end
context 'when resource is not owned' do
it 'responds with 404'
end
end
«Expect» contre «Should»
Dans vos nouveaux projets, utiliser la syntaxe expect
.
mauvais
it 'creates a resource' do
response.should respond_with_content_type(:json)
end
bon
it 'creates a resource' do
expect(response).to respond_with_content_type(:json)
end
Configurer RSpec pour qu'il n'accepte que la nouvelle syntaxe dans vos nouveaux projets, et ainsi éviter d'avoir les deux syntaxes qui cohabitent.
bon
# spec_helper.rb
RSpec.configure do |config|
# ...
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
Dans les projets existants, utiliser transpec pour convertir vos tests vers la nouvelle syntaxe.
Dans les attentes d'une seule ligne ou avec un sujet implicite continuer d'utiliser la syntaxe «should» plus d'informations
Plus d'informations à propos de la nouvelle syntaxe peuvent être trouvées ici et ici.
Utiliser «subject"
Si vous avez plusieurs tests qui possèdent le même sujet, utiliser subject{}
pour éviter la duplication de code.
mauvais
it { expect(assigns('message')).to match /it was born in Belville/ }
bon
subject { assigns('message') }
it { should match /it was born in Billville/ }
Vous pouvez également utiliser des sujets nommés avec RSpec.
bon
subject(:hero) { Hero.first }
it "carries a sword" do
expect(hero.equipment).to include "sword"
end
En apprendre plus sur «subject».
Utiliser «let» et «let!»
Lors de l'assignation d'une variable, vous pouvez à la place de créer un bloc before
utiliser let
. L'utilisation de let
rend la variable longue à charger la première fois, mais elle est ensuite mise en cache jusqu'à la fin du test. Une très bonne description de let
peut être trouvée dans cette réponse stackoverflow.
mauvais
describe '#type_id' do
before { @resource = FactoryGirl.create :device }
before { @type = Type.find @resource.type_id }
it 'sets the type_id field' do
expect(@resource.type_id).to equal(@type.id)
end
end
bon
describe '#type_id' do
let(:resource) { FactoryGirl.create :device }
let(:type) { Type.find resource.type_id }
it 'sets the type_id field' do
expect(resource.type_id).to equal(type.id)
end
end
Utiliser let
pour initialiser des actions longues à charger pour tester vos spécifications.
bon
context 'when updates a not existing property value' do
let(:properties) { { id: Settings.resource_id, value: 'on'} }
def update
resource.properties = properties
end
it 'raises a not found error' do
expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
end
end
Utiliser let!
si vous souhaitez définir une variable quand le bloc est défini. C'est utile lorsque vous peuplez votre base de données pour tester des requêtes ou des portées.
Ici un exemple de «let».
bon
# this:
let(:foo) { Foo.new }
# is very nearly equivalent to this:
def foo
@foo ||= Foo.new
end
En apprendre plus sur «let».
«Mock or not to mock»
Ne pas (trop) utiliser les «mocks» et tester le comportement réel quand c'est possible. Tester les cas réels est utile lors de la mise à jour du flux de votre application.
bon
# simulate a not found resource
context "when not found" do
before do
allow(Resource).to receive(:where).with(created_from: params[:id])
.and_return(false)
end
it { should respond_with 404 }
end
Utiliser des «mocks» rend vos spécifications plus rapides mais elles sont plus difficiles à utiliser. Vous avez besoin de les maîtriser pour bien les utiliser. Lire plus à propos.
Créer seulement les données requises
Si vous avez déjà travaillé sur des projets de taille moyenne (mais également sur des petits), les tests peuvent être long à exécuter. Pour résoudre ce problème, c'est important de ne pas charger plus de données que nécessaire. Si vous pensez que vous avez besoin de douzaines d'enregistrements, vous avez probablement tort.
bon
describe "User"
describe ".top" do
before { FactoryGirl.create_list(:user, 3) }
it { expect(User.top(2)).to have(2).item }
end
end
Utiliser les «factories» et ne pas utiliser les «fixtures»
Ne pas utiliser les «fixtures» car elles sont difficiles à maîtriser. Utiliser des "«factories" à la place qui permettent de réduire la verbosité dans la création de nouvelles données.
mauvais
user = User.create(
name: 'Genoveffa',
surname: 'Piccolina',
city: 'Billyville',
birth: '17 Agoust 1982',
active: true
)
bon
user = FactoryGirl.create :user
Quand nous parlons de tests unitaires la bonne pratique devrait de n'utiliser ni les «fixtures», ni les «factories». Mettez le maximum de logique dans des bibliothèques qui peuvent être testées sans complexité. Lire plus dans cet article
En apprendre plus sur Factory Girl.
Des «matchers» facile à lire
Utilisez des «matchers» lisibles et jetez un œil sur rspec matchers.
mauvais
lambda { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
bon
expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
Exemples partagés
Écrire des tests est bien, et vous devriez gagner en confiance chaque jour. Mais vous devriez commencer à voir de la duplication de code. Utiliser les exemples partagés pour réduire le nombre de répétitions.
mauvais
describe 'GET /devices' do
let!(:resource) { FactoryGirl.create :device, created_from: user.id }
let(:uri) { '/devices' }
context 'when shows all resources' do
let!(:not_owned) { FactoryGirl.create factory }
it 'shows all owned resources' do
page.driver.get uri
expect(page.status_code).to be(200)
contains_owned_resource resource
does_not_contain_resource not_owned
end
end
describe '?start=:uri' do
it 'shows the next page' do
page.driver.get uri, start: resource.uri
expect(page.status_code).to be(200)
contains_resource resources.first
expect(page).to_not have_content resource.id.to_s
end
end
end
bon
describe 'GET /devices' do
let!(:resource) { FactoryGirl.create :device, created_from: user.id }
let(:uri) { '/devices' }
it_behaves_like 'a listable resource'
it_behaves_like 'a paginable resource'
it_behaves_like 'a searchable resource'
it_behaves_like 'a filterable list'
end
De notre propre expérience, les exemples partagés sont utilisés principalement dans les contrôleurs. Les modèles sont très différents les un des autres, ils partagent peu de logique.
En apprendre plus à propos desexemples partagés.
Tester ce que vous voyez
Tester profondément vos modèles, et le comportement de votre application (tests d'intégration). N'ajouter pas de tests inutiles pour vos contrôleurs.
Quand j'ai commencé à tester mes applications je testais les contrôleurs, maintenant je ne le fais plus. Maintenant je crée seulement des tests d'intégration en utilisant RSpec et Capybara. Pourquoi ? Parce que je crois que vous devriez tester ce que vous voyez et parce que tester les contrôleurs est une étape supplémentaire qui n'est pas nécessaire. Vous devriez trouver que la plupart de vos tests concernant les modèles et les tests d'intégration peuvent être facilement groupés avec des exemples partagés, et ainsi construire une suite de tests claire et lisible.
C'est un débat ouvert dans la communauté Ruby, avec des deux côtés de très bons arguments supportant leur idée. Les personnes qui supportent le besoin de tester les contrôleurs devraient dire que les tests d'intégration ne couvrent pas tous les cas et qu'ils sont lent.
Les deux ont tort. Vous pouvez facilement couvrir tous les cas et vous pouvez lancer seulement un seul fichier spec en utilisant des outils automatisés comme Guard. Dans cette voie, vous devriez pouvoir lancer seulement les spécifications que vous avez besoin de tester rapidement sans ralentir votre flux de tâches.
Ne pas utiliser «should»
N'utiliser pas "should" quand vous décrivez vos tests. Utiliser la troisième personne au présent simple. Encore mieux commencer à utiliser la nouvelle syntaxe «should» syntax.
mauvais
it 'should not change timings' do
consumption.occur_at.should == valid.occur_at
end
bon
it 'does not change timings' do
expect(consumption.occur_at).to equal(valid.occur_at)
end
Voir le «should_not» gem pour utiliser cette solution dans RSpec et le «should_clean» gem pour nettoyer les exemples RSpec qui commencent par «should».
Exécuter automatiquement les tests avec Guard
Lancer la totalité des tests à chaque changement devrait être pesant. Cela prend du temps, et casse votre flux de tâches. Avec Guard, vous pouvez automatiser votre suite de tests en lançant seulement les tests liés à la spécification sur laquelle vous êtes en train de travailler.
bon
bundle exec guard
bon
guard 'rspec', cli: '--drb --format Fuubar --color', version: 2 do
# run every updated spec file
watch(%r{^spec/.+_spec\.rb$})
# run the lib specs when a file in lib/ changes
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
# run the model specs related to the changed model
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
# run the view specs related to the changed view
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
# run the integration specs related to the changed controller
watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# run all integration tests when application controller change
watch('app/controllers/application_controller.rb') { "spec/requests" }
end
Guard est un bon outil mais il ne couvre pas tous les besoins. Quelquefois votre flux TDD travaille mieux avec des raccourcis clavier qui rendent le lancement d'exemple unitaire aisé. Vous pouvez alors utiliser les tâches rake pour lancer la totalité des tests avant de pousser votre code. ici les raccourcis vim.
En apprendre plus sur guard-rspec.
Des tests plus rapides en préchargeant Rails
Quand vous lancez un test avec Rails l'application Rails entière est chargée. Ce qui peut prendre du temps et casser votre flux de développement. Pour résoudre ce problème utiliser des solutions comme Zeus, Spin ou Spork. Ces solutions devraient préchargées toutes les bibliothèques qui n'ont pas changées et recharger les contrôleurs, modèles, vues et «factories» ainsi que les fichiers qui changent régulièrement.
Vous pouvez trouver un spec helper et un fichier de configuration Guardfile basé sur Spork. Avec cette configuration l'application ne sera entièrement rechargée que si un fichier préchargé (comme initializers) change et devrait lancer les tests uniques très rapidement.
L'inconvénient d'utiliser Spork est que vous risquez de perdre quelques heures à essayer de comprendre pourquoi un fichier n'est pas rechargé. Si vous avec quelques exemples de code utilisant Spin or n'importe quelles autres solutions faites le nous savoir.
Vous pouvez trouver ici un fichier de configurationGuardfile pour utiliser Zeus. Le spec_helper n'a pas besoin d'être modifié, mais vous devriez lancer `zeus start` dans une console pour démarrer le serveur zeus avant de lancer vos tests.
Bien que Zeus a une approche moins agressive que Spork, l'inconvénient majeur est ses exigences strictes; Ruby 1.9.3+ (recommended using backported GC from Ruby 2.0) et un système d'exploitation qui supporte FSEVENTS ou inotify.
Ces solutions subissent de nombreuses critiques. Ces bibliothèques sont un pansement à un problème qui devrait être résolu par une meilleure conception, et par le chargement intentionnel des dépendances nécessaires. En apprendre plus en lisant la conversation liée.
Émuler des requêtes HTTP
Quelquefois vous avez besoin d’accéder à des services web externes. Dans ces cas vous ne pouvez pas relier au service réel mais vous pouvez le simuler avec des solutions comme webmock.
bon
context "with unauthorized access" do
let(:uri) { 'http://api.lelylan.com/types' }
before { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
it "gets a not authorized notification" do
page.driver.get uri
expect(page).to have_content 'Access denied'
end
end
En apprendre plus sur webmock and VCR. Ici une bonne présentation qui explique comment les utiliser ensemble.
Formater intelligemment
Utiliser un format qui peut vous donner des informations utiles sur la suite de tests. Je trouve que fuubar est vraiment intéressant. Pour le faire fonctionner ajouter le gem et ajouter fuubar comme format par défaut dans votre fichier Guardfile.
bon
# Gemfile
group :development, :test do
gem 'fuubar'
bon
# Guardfile
guard 'rspec' do
# ...
end
bon
# .rspec
--drb
--format Fuubar
--color
En apprendre plus sur fuubar.
Livres
<%= render "partials/books" %>Présentations
Ressources en ligne
<%= render "partials/links" %>Screencasts
<%= render "partials/screencasts" %>Documentation
<%= render "partials/libraries" %>Styleguide
Nous sommes à la recherche des meilleures pratiques pour écrire des spécifications "facile à lire". Pour le moment, la Mongoid test suite est un bon début. Elle utilise un style clair, et des spécifications facile à lire, qui suivent les conseils décrit ici.
Contribuer à Better Specs
Il s'agit d'un projet open-source. Si quelque chose est manquant ou incorrect crée une issue pour discuter du sujet. Vérifier les problèmes suivis:
- Multilingues (crée une «issue» si vous voulez traduire ce guide)
Crédits
Ce document a été préparé par Andrea Reginato. Un remerciement spéciale à l'équipe Lelylan Team. Traduit par Florent Ferry. Ce document est sous licence MIT.
Aidez-nous
Si ces conseils vous ont aidés à améliorer votre façon d'écrire des tests, vous pouvez faire un don de $9, et ainsi contribuer à l'amélioration de ce site.