Comment sécher vos tests RSpec à l'aide d'exemples partagés

" Donnez-moi six heures pour abattre un arbre et je passerai les quatre premières à affûter la hache." - Abraham Lincoln

Lorsque j'ai refactoré un projet il y a quelques semaines, j'ai passé la plupart de mon temps à rédiger des spécifications. Après avoir écrit plusieurs cas de test similaires pour certaines API, j'ai commencé à me demander si je pourrais peut-être me débarrasser d'une grande partie de cette duplication.

Je me suis donc lancé dans la lecture des meilleures pratiques pour les tests de DRYing (Don't Repeat Yourself). Et c'est ainsi que j'ai connu shared exampleset shared contexts.

Dans mon cas, j'ai fini par utiliser des exemples partagés. Et voici ce que j'ai appris jusqu'à présent en les appliquant.

Lorsque vous avez plusieurs spécifications décrivant un comportement similaire, il peut être préférable d'extraire des exemples redondants shared exampleset de les utiliser dans plusieurs spécifications.

Supposons que vous ayez deux modèles User et Post , et qu'un utilisateur puisse avoir de nombreux posts. Les utilisateurs doivent pouvoir afficher la liste des utilisateurs et des publications. La création d'une action d'index dans les contrôleurs des utilisateurs et des messages servira cet objectif.

Tout d'abord, écrivez les spécifications de votre action d'index pour le contrôleur des utilisateurs. Il aura la responsabilité de récupérer les utilisateurs et de les rendre avec une mise en page appropriée. Ensuite, écrivez suffisamment de code pour que les tests réussissent.

# users_controller_spec.rbdescribe "GET #index" do before do 5.times do FactoryGirl.create(:user) end get :index end it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) } it { expect(assigns(:users)).to match(User.all) }end
# users_controller.rbclass UsersController < ApplicationController .... def index @users = User.all end ....end

En règle générale, l'action d'index de n'importe quel contrôleur récupère et agrège les données de quelques ressources selon les besoins. Il ajoute également la pagination, la recherche, le tri, le filtrage et la portée.

Enfin, toutes ces données sont présentées aux vues via HTML, JSON ou XML à l'aide d'API. Pour simplifier mon exemple, les actions d'index des contrôleurs vont simplement récupérer des données, puis les afficher via des vues.

Il en va de même pour l'action d'index dans le contrôleur posts:

describe "GET #index" do before do 5.times do FactoryGirl.create(:post) end get :index end it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) } it { expect(assigns(:posts)).to match(Post.all) }end
# posts_controller.rbclass PostsController < ApplicationController .... def index @posts = Post.all end ....end

Les tests RSpec écrits à la fois pour les utilisateurs et pour le contrôleur de postes sont très similaires. Dans les deux contrôleurs, nous avons:

  • Le code de réponse - doit être «OK»
  • Les deux actions d'index doivent rendre une vue partielle ou une vue correcte - dans notre cas index
  • Les données que nous souhaitons rendre, telles que les publications ou les utilisateurs

SÉCHONS les spécifications de notre action d'index en utilisant shared examples.

Où mettre vos exemples partagés

J'aime placer des exemples partagés dans le répertoire specs / support / shared_examples afin que tous les shared examplefichiers associés soient chargés automatiquement.

Vous pouvez lire sur d'autres conventions couramment utilisées pour localiser votre shared examplesici: documentation des exemples partagés

Comment définir un exemple partagé

Votre action d'indexation doit répondre avec un code de réussite 200 (OK) et afficher votre modèle d'index.

RSpec.shared_examples "index examples" do it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) }end

En plus de vos itblocs - et avant et après vos hooks - vous pouvez ajouter des letblocs, du contexte et décrire des blocs, qui peuvent également être définis à l'intérieur shared examples.

Personnellement, je préfère garder les exemples partagés simples et concis, et ne pas ajouter de contextes et laisser des blocs. Le shared examplesbloc accepte également les paramètres, que je couvrirai ci-dessous.

Comment utiliser des exemples partagés

L'ajout include_examples "index examples"de spécifications de contrôleur à vos utilisateurs et publications inclut des «exemples d'index» à vos tests.

# users_controller_spec.rbdescribe "GET #index" do before do 5.times do FactoryGirl.create(:user) end get :index end include_examples "index examples" it { expect(assigns(:users)).to match(User.all) }end
# similarly, in posts_controller_spec.rbdescribe "GET #index" do before do 5.times do FactoryGirl.create(:post) end get :index end include_examples "index examples" it { expect(assigns(:posts)).to match(Post.all) }end

Vous pouvez également utiliser it_behaves_likeou à la it_should_behaves_likeplace de include_examplesdans ce cas. it_behaves_likeet it_should_behaves_likesont en fait des alias, et fonctionnent de la même manière, ils peuvent donc être utilisés de manière interchangeable. Mais include_exampleset it_behaves_likesont différents.

Comme indiqué dans la documentation officielle:

  • include_examples - comprend des exemples dans le contexte actuel
  • it_behaves_likeet it_should_behave_likeinclure les exemples dans un contexte imbriqué

Pourquoi cette distinction est-elle importante?

La documentation de RSpec donne une réponse correcte:

Lorsque vous incluez plusieurs fois des exemples paramétrés dans le contexte actuel, vous pouvez remplacer les définitions de méthode précédentes et la dernière déclaration gagne.

Ainsi, lorsque vous êtes confronté à une situation où les exemples paramétrés contiennent des méthodes qui sont en conflit avec d'autres méthodes dans le même contexte, vous pouvez les remplacer include_examplespar it_behaves_likemethod. Cela créera un contexte imbriqué et évitera ce genre de situations.

Consultez la ligne suivante dans les spécifications du contrôleur de vos utilisateurs et publiez les spécifications du contrôleur:

it { expect(assigns(:users)).to match(User.all) }it { expect(assigns(:posts)).to match(Post.all) }

Maintenant, les spécifications de votre contrôleur peuvent être re-factorisées davantage en passant des paramètres à l'exemple partagé comme ci-dessous:

# specs/support/shared_examples/index_examples.rb
# here assigned_resource and resource class are parameters passed to index examples block RSpec.shared_examples "index examples" do |assigned_resource, resource_class| it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) } it { expect(assigns(assigned_resource)).to match(resource_class.all) }end

Maintenant, apportez les modifications suivantes à vos utilisateurs et publiez les spécifications du contrôleur:

# users_controller_spec.rbdescribe "GET #index" do before do ... end include_examples "index examples", :users, User.allend
# posts_controller_spec.rbdescribe "GET #index" do before do ... end include_examples "index examples", :posts, Post.allend

Désormais, les spécifications du contrôleur semblent propres, moins redondantes et, plus important encore, DRY. De plus, ces exemples d'index peuvent servir de structures de base pour concevoir l'action d'index d'autres contrôleurs.

Conclusion

En déplaçant les exemples courants dans un fichier séparé, vous pouvez éliminer la duplication et améliorer la cohérence des actions de votre contrôleur dans toute votre application. Ceci est très utile dans le cas de la conception d'API, car vous pouvez utiliser la structure existante des tests RSpec pour concevoir des tests et créer des API qui adhèrent à votre structure de réponse commune.

La plupart du temps, lorsque je travaille avec des API, j'utilise shared examplespour me fournir une structure commune pour concevoir des API similaires.

N'hésitez pas à partager comment vous séchez vos spécifications en utilisant shared examples.