Tester: comment se concentrer sur le comportement plutôt que sur la mise en œuvre sans perdre de vitesse?

Il semble qu’il y ait deux approches totalement différentes en matière de test et j’aimerais les citer toutes les deux.

Le fait est que ces opinions ont été formulées il y a cinq ans (2007) et je suis intéressé par ce qui a changé depuis lors et par quel moyen dois-je aller.

Brandon Keepers :

La théorie est que les tests sont censés être indépendants de l’implémentation. Cela conduit à des tests moins fragiles et teste réellement le résultat (ou le comportement).

Avec RSpec, j’ai l’impression que l’approche commune consistant à moquer complètement vos modèles pour tester vos contrôleurs finit par vous obliger à vous pencher sur la mise en œuvre de votre contrôleur.

Cela en soi n’est pas trop grave, mais le problème est qu’il est trop intégré dans le contrôleur pour déterminer comment le modèle est utilisé. Pourquoi est-ce important que mon contrôleur appelle Thing.new? Et si mon contrôleur décide de prendre le Thing.create! et voie de sauvetage? Que faire si mon modèle a une méthode d’initialisation spéciale, comme Thing.build_with_foo? Mes spécifications de comportement ne doivent pas échouer si je modifie l’implémentation.

Ce problème s’aggrave encore lorsque vous disposez de ressources nestedes et que vous créez plusieurs modèles par contrôleur. Certaines de mes méthodes de configuration ont une longueur de 15 lignes ou plus et sont TRÈS fragiles.

L’intention de RSpec est d’isoler complètement la logique de votre contrôleur de vos modèles, ce qui semble bien en théorie, mais se heurte presque à une stack intégrée comme Rails. Surtout si vous pratiquez la discipline maigre contrôleur / modèle de graisse, la quantité de logique dans le contrôleur devient très petite, et la configuration devient énorme.

Alors, qu’est-ce qu’un BDD-wannabe à faire? En prenant du recul, le comportement que je veux vraiment tester n’est pas que mon contrôleur appelle Thing.new, mais compte tenu des parameters X, cela crée une nouvelle chose et lui redirige.

David Chelimsky:

Tout est question de compromis.

Le fait que AR choisisse l’inheritance plutôt que la délégation nous place dans une liaison de test – nous devons être couplés à la firebase database OU nous devons être plus intimes avec l’implémentation. Nous acceptons ce choix de conception car nous tirons des avantages de l’expressivité et de la souplesse.

Face au dilemme, j’ai choisi des tests plus rapides au prix d’un peu plus fragiles. Vous choisissez des tests moins fragiles au prix de leur léger ralentissement. C’est un compromis de toute façon.

En pratique, je lance les tests des centaines, voire des milliers de fois par jour (j’utilise l’autotest et franchis des étapes très précises) et je change si j’utilise «new» ou «create» presque jamais. Également en raison des étapes granulaires, les nouveaux modèles qui apparaissent sont assez volatils au début. L’approche valid_thing_attrs minimise un peu la douleur, mais cela signifie que chaque nouveau champ obligatoire signifie que je dois modifier valid_thing_attrs.

Mais si votre approche fonctionne pour vous dans la pratique, alors c’est bon! En fait, je vous recommande fortement de publier un plug-in avec des générateurs qui produisent les exemples comme vous les aimez. Je suis sûr que beaucoup de gens en profiteraient.

Ryan Bates :

Par curiosité, à quelle fréquence utilisez-vous des simulacres dans vos tests / spécifications? Peut-être que je fais quelque chose de mal, mais je trouve cela très ressortingctif. Depuis que je suis passé à rSpec il y a plus d’un mois, je fais ce qu’ils recommandent dans les documents où le contrôleur et les couches d’affichage ne touchent pas du tout la firebase database et les modèles sont complètement dérobés. Cela vous donne une bonne accélération de la vitesse et rend certaines choses plus faciles, mais je trouve que les inconvénients de le faire dépassent largement les avantages. Depuis que je me suis moqué de moi, mes spécifications se sont transformées en cauchemar de maintenance. Les spécifications sont destinées à tester le comportement, pas l’implémentation. Je ne m’inquiète pas si une méthode a été appelée Je veux juste m’assurer que la sortie résultante est correcte. Parce que les moqueries rendent les spécifications difficiles pour l’implémentation, cela rend les refactorisations simples (qui ne changent pas le comportement) impossibles sans devoir constamment revenir en arrière et “réparer” les spécifications. Je suis très d’avis sur ce que devraient couvrir les spécifications / tests. Un test ne doit casser que lorsque l’application se casse. C’est l’une des raisons pour lesquelles je teste à peine la couche d’affichage car je la trouve trop rigide. Cela conduit souvent à des tests qui se brisent sans que l’application ne se casse lorsque l’on change de petites choses dans la vue. Je trouve le même problème avec les simulacres. En plus de tout cela, je viens de me rendre compte aujourd’hui que moquer / stubber une méthode de classe (parfois) rest entre les spécifications. Les spécifications doivent être autonomes et non influencées par d’autres spécifications. Cela enfreint cette règle et conduit à des bogues difficiles. Qu’est-ce que j’ai appris de tout ça? Soyez prudent lorsque vous utilisez moqueur. Le stubbing n’est pas aussi mauvais, mais présente toujours les mêmes problèmes.

J’ai pris les dernières heures et enlevé presque tous les moindres de mes spécifications. J’ai également fusionné le contrôleur et visualisé les spécifications en une seule en utilisant “integr_views” dans les spécifications du contrôleur. Je charge également tous les appareils pour chaque spécification de contrôleur, il y a donc des données de test pour remplir les vues. Le résultat final? Mes spécifications sont plus courtes, plus simples, plus cohérentes, moins rigides et elles testent l’ensemble de la stack (modèle, vue, contrôleur) afin qu’aucun bogue ne puisse passer à travers les fissures. Je ne dis pas que c’est la “bonne” voie pour tout le monde. Si votre projet nécessite un cas très ssortingct, ce ne sera peut-être pas pour vous, mais dans mon cas, il s’agit d’un monde meilleur que celui que j’avais avant d’utiliser des simulacres. Je pense toujours que le stubbing est une bonne solution dans quelques endroits alors je le fais toujours.

Je pense que les trois opinions sont encore complètement valables. Ryan et moi étions aux sockets avec la maintenabilité de la moquerie, tandis que David estimait que le compromis de maintenance en valait la peine pour l’augmentation de la vitesse.

Mais ces compromis sont les symptômes d’un problème plus profond, auquel David a fait allusion en 2007: ActiveRecord. La conception d’ActiveRecord vous encourage à créer des objects dieu qui en font trop, en savent trop sur le rest du système et ont trop de surface. Cela conduit à des tests qui ont trop à tester, en savent trop sur le rest du système et sont trop lents ou fragiles.

Alors, quelle est la solution? Séparez autant de votre application du cadre que possible. Écrivez beaucoup de petites classes qui modélisent votre domaine et n’héritent de rien. Chaque object doit avoir une surface limitée (pas plus de quelques méthodes) et des dépendances explicites transmises via le constructeur.

Avec cette approche, je n’ai écrit que deux types de tests: les tests unitaires isolés et les tests de système à stack complète. Dans les tests d’isolement, je me moque de tout ce qui n’est pas l’object testé. Ces tests sont incroyablement rapides et ne nécessitent souvent pas le chargement de tout l’environnement Rails. Les tests de la stack complète exercent l’ensemble du système. Ils sont douloureusement lents et donnent une rétroaction inutile lorsqu’ils échouent. J’écris aussi peu que nécessaire, mais suffisamment pour me donner l’assurance que tous mes objects éprouvés s’intègrent bien.

Malheureusement, je ne peux pas vous montrer un exemple de projet qui le fait bien (encore). J’en parle un peu dans ma présentation sur Why Our Code Smells , je regarde la présentation de Corey Haines sur les tests Fast Rails , et je recommande fortement de lire les logiciels orientés object orientés par les tests .

Merci d’avoir compilé les citations de 2007. C’est amusant de regarder en arrière.

Mon approche actuelle des tests est abordée dans cet épisode de RailsCasts avec lequel je suis plutôt satisfait. En résumé, j’ai deux niveaux de tests.

  • Haut niveau: j’utilise les spécifications de requête dans RSpec, Capybara et VCR. Les tests peuvent être marqués pour exécuter JavaScript selon les besoins. On évite de se moquer car le but est de tester la stack entière. Chaque action du contrôleur est testée au moins une fois, peut-être quelques fois.

  • Niveau bas: c’est là que toute la logique complexe est testée – principalement les modèles et les assistants. J’évite aussi de me moquer de moi. Les tests frappent la firebase database ou les objects environnants si nécessaire.

Notez qu’il n’y a pas de contrôleur ou de spécifications de vue. Je pense que celles-ci sont couvertes de manière adéquate dans les spécifications de demande.

Comme il y a peu de moquerie, comment je garde les tests rapidement? Voici quelques conseils.

  • Eviter une logique de twigment excessive dans les tests de haut niveau. Toute logique complexe doit être déplacée au niveau inférieur.

  • Lors de la génération d’enregistrements (comme avec Factory Girl), utilisez d’abord build et ne changez que pour create si nécessaire.

  • Utilisez Guard avec Spork pour passer le temps de démarrage de Rails. Les tests pertinents sont souvent effectués en quelques secondes après la sauvegarde du fichier. Utilisez une balise :focus dans RSpec pour limiter les tests exécutés sur une zone spécifique. S’il s’agit d’une suite de tests volumineuse, définissez all_after_pass: false, all_on_start: false dans le fichier de garde pour ne les exécuter que si nécessaire.

  • J’utilise plusieurs assertions par test. L’exécution du même code de configuration pour chaque assertion augmente considérablement le temps de test. RSpec imprimera la ligne qui a échoué, il est donc facile de la localiser.

Je trouve moquant ajoute de la fragilité aux tests, c’est pourquoi je l’évite. Certes, cela peut être une aide précieuse pour la conception OO, mais dans la structure d’une application Rails, cela ne semble pas aussi efficace. Au lieu de cela, je compte beaucoup sur le refactoring et laisse le code lui-même me dire comment la conception devrait aller.

Cette approche fonctionne mieux sur les applications Rails de petite et moyenne taille sans une logique de domaine complexe et étendue.

De bonnes questions et de bonnes discussions. @ryanb et @bkeepers mentionnent qu’ils n’écrivent que deux types de tests. Je prends une approche similaire, mais j’ai un troisième type de test:

  • Tests unitaires: tests isolés, généralement, mais pas toujours, contre des objects en rbuy simples. Mes tests unitaires ne concernent pas la firebase database, les appels API tiers ou tout autre élément externe.
  • Tests d’intégration: ceux-ci sont toujours axés sur le test d’une classe; les différences sont qu’ils intègrent cette classe aux éléments externes que j’évite dans mes tests unitaires. Mes modèles auront souvent des tests unitaires et des tests d’intégration, où les tests unitaires se concentrent sur la logique pure pouvant être testée sans impliquer la firebase database, et les tests d’intégration impliqueront la firebase database. De plus, j’ai tendance à tester des wrappers API tiers avec des tests d’intégration, en utilisant VCR pour garder les tests rapides et déterministes, mais en laissant mes CI construire pour les requêtes HTTP (pour attraper les modifications de l’API).
  • Tests d’acceptation: tests de bout en bout, pour une fonctionnalité complète. Il ne s’agit pas seulement de tester l’interface utilisateur via capybara; Je fais la même chose dans mes gemmes, qui peuvent ne pas avoir d’interface HTML. Dans ces cas, cela exerce tout ce que la gemme fait de bout en bout. J’ai aussi tendance à utiliser le magnétoscope dans ces tests (s’ils font des requêtes HTTP externes) et, comme dans mes tests d’intégration, ma construction de CI est configurée pour rendre les requêtes HTTP réelles.

En ce qui concerne les moqueries, je n’ai pas une approche unique. J’ai définitivement surestimé dans le passé, mais je trouve toujours que c’est une technique très utile, surtout quand on utilise quelque chose comme rspec-fire . En général, je me moque des collaborateurs jouant des rôles librement (en particulier si je les possède, et ils sont des objects de service) et j’essaie de l’éviter dans la plupart des cas.

Le plus grand changement apporté à mes tests depuis un an a sans doute été inspiré par DAS : alors que j’avais un spec_helper.rb qui spec_helper.rb l’ensemble de l’environnement, je ne charge plus que le test de classe (et les dépendances). En plus de la vitesse de test améliorée (qui fait une énorme différence!), Cela m’aide à identifier le moment où mes classes sous test entraînent trop de dépendances.