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

Discuter de ce conseil →

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».

Discuter de ce conseil →

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

Discuter de ce conseil →

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
Discuter de ce conseil →

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

Discuter de ce conseil →

«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.

Discuter de ce conseil →

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».

Discuter de ce conseil →

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».

Discuter de ce conseil →

«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 { allow(Resource).to receive(:where).with(created_from: params[:id]).and_return(false) }
  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.

Discuter de ce conseil →

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

Discuter de ce conseil →

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.

Discuter de ce conseil →

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

Discuter de ce conseil →

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.

Discuter de ce conseil →

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.

Discuter de ce conseil →

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».

Discuter de ce conseil →

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
Ici vous pouvez voir un exemple d'un fichier Guardfile avec des règles de rechargement basiques.

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.

Discuter de ce conseil →

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.

Discuter de ce conseil →

É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.

Discuter de ce conseil →

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.

Discuter de ce conseil →

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)

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.