web-dev-qa-db-fra.com

Est-il suffisant d'utiliser des tests d'acceptation et d'intégration au lieu de tests unitaires?

Brève introduction à cette question. J'utilise maintenant TDD et dernièrement BDD depuis plus d'un an maintenant. J'utilise des techniques comme la moquerie pour rendre l'écriture de mes tests plus efficace. Dernièrement, j'ai commencé un projet personnel pour écrire un petit programme de gestion de l'argent pour moi. Comme je n'avais pas de code hérité, c'était le projet parfait pour commencer avec TDD. Malheureusement, je n'ai pas tellement vécu la joie du TDD. Cela a même tellement gâché mon plaisir que j'ai abandonné le projet.

Quel était le problème? Eh bien, j'ai utilisé l'approche de type TDD pour laisser les tests/exigences faire évoluer la conception du programme. Le problème était que plus de la moitié du temps de développement pour les tests d'écriture/refactorisation. Donc, à la fin, je ne voulais pas implémenter plus de fonctionnalités car je devrais refactoriser et écrire sur de nombreux tests.

Au travail, j'ai beaucoup de code hérité. Ici j'écris de plus en plus de tests d'intégration et d'acceptation et de moins en moins de tests unitaires. Cela ne semble pas être une mauvaise approche car les bogues sont principalement détectés par les tests d'acceptation et d'intégration.

Mon idée était que je pourrais finalement écrire plus de tests d'intégration et d'acceptation que de tests unitaires. Comme je l'ai dit pour détecter les bogues, les tests unitaires ne sont pas meilleurs que les tests d'intégration/d'acceptation. Les tests unitaires sont également bons pour la conception. Comme j'en écrivais beaucoup, mes cours sont toujours conçus pour être testables. De plus, l'approche consistant à laisser les tests/exigences guider la conception conduit dans la plupart des cas à une meilleure conception. Le dernier avantage des tests unitaires est qu'ils sont plus rapides. J'ai écrit suffisamment de tests d'intégration pour savoir qu'ils peuvent être presque aussi rapides que les tests unitaires.

Après avoir parcouru le Web, j'ai découvert qu'il y avait des idées très similaires aux miennes mentionnées ici et . Que penses tu de cette idée?

Modifier

En répondant aux questions, un exemple où la conception était bonne, mais j'avais besoin d'une refactorisation énorme pour la prochaine exigence:

Au début, il y avait certaines exigences pour exécuter certaines commandes. J'ai écrit un analyseur de commandes extensible - qui analysait les commandes d'une sorte d'invite de commandes et appelait la bonne sur le modèle. Le résultat était représenté dans une classe de modèle de vue: First design

Il n'y avait rien de mal ici. Toutes les classes étaient indépendantes les unes des autres et je pouvais facilement ajouter de nouvelles commandes, afficher de nouvelles données.

La condition suivante était que chaque commande ait sa propre représentation de vue - une sorte d'aperçu du résultat de la commande. J'ai repensé le programme pour obtenir une meilleure conception pour la nouvelle exigence: Second design

C'était également une bonne chose car maintenant chaque commande a son propre modèle de vue et donc son propre aperçu.

Le fait est que l'analyseur de commandes a été modifié pour utiliser une analyse basée sur des jetons des commandes et a été privé de sa capacité à exécuter les commandes. Chaque commande a son propre modèle de vue et le modèle de vue de données ne connaît que le modèle de vue de commande actuel qui connaît les données qui doivent être affichées.

Tout ce que je voulais savoir à ce stade, c'est si le nouveau design ne cassait aucune exigence existante. Je n'ai eu à modifier AUCUN de mes tests d'acceptation. J'ai dû refactoriser ou supprimer presque TOUS les tests unitaires, ce qui représentait une énorme charge de travail.

Ce que je voulais montrer ici est une situation courante qui s'est souvent produite pendant le développement. Il n'y avait aucun problème avec les anciens ou les nouveaux designs, ils ont juste changé naturellement avec les exigences - comment je l'ai compris, c'est un avantage de TDD, que le design évolue.

Conclusion

Merci pour toutes les réponses et discussions. En résumé de cette discussion, j'ai pensé à une approche que je testerai avec mon prochain projet.

  • Tout d'abord, j'écris tous les tests avant d'implémenter quelque chose comme je l'ai toujours fait.
  • Pour les exigences, j'écris d'abord des tests d'acceptation qui testent l'ensemble du programme. Ensuite, j'écris des tests d'intégration pour les composants où j'ai besoin de mettre en œuvre l'exigence. S'il y a un composant qui travaille en étroite collaboration avec un autre composant pour implémenter cette exigence, j'écrirais également des tests d'intégration où les deux composants sont testés ensemble. Enfin et surtout si je dois écrire un algorithme ou toute autre classe avec une permutation élevée - par exemple un sérialiseur - j'écrirais des tests unitaires pour ces classes particulières. Toutes les autres classes ne sont pas testées mais tous les tests unitaires.
  • Pour les bogues, le processus peut être simplifié. Normalement, un bogue est causé par un ou deux composants. Dans ce cas, j'écrirais un test d'intégration pour les composants qui teste le bogue. S'il s'agissait d'un algorithme, je n'écrirais qu'un test unitaire. S'il n'est pas facile de détecter le composant où le bogue se produit, j'écrirais un test d'acceptation pour localiser le bogue - cela devrait être une exception.
63
Yggdrasil

C'est comparer des oranges et des pommes.

Tests d'intégration, tests d'acceptation, tests unitaires, tests de comportement - ce sont tous des tests et ils vous aideront tous à améliorer votre code mais ils sont également très différents.

Je vais passer en revue chacun des différents tests à mon avis et j'espère expliquer pourquoi vous avez besoin d'un mélange de tous:

Tests d'intégration:

Testez simplement que les différents composants de votre système s'intègrent correctement - par exemple - vous simulez peut-être une demande de service Web et vérifiez que le résultat revient. J'utiliserais généralement des données statiques réelles (ish) et des dépendances simulées pour m'assurer qu'elles peuvent être vérifiées de manière cohérente.

Tests d'acceptation:

Un test d'acceptation doit être directement corrélé à un cas d'utilisation métier. Il peut être énorme ("les transactions sont soumises correctement") ou minuscule ("le filtre filtre avec succès une liste") - cela n'a pas d'importance; ce qui importe, c'est qu'il devrait être explicitement lié à une exigence spécifique de l'utilisateur. J'aime me concentrer sur ceux-ci pour le développement piloté par les tests, car cela signifie que nous avons un bon manuel de référence de tests sur les user stories pour dev et qa à vérifier.

Tests unitaires:

Pour les petites unités de fonctionnalité discrètes qui peuvent ou non constituer une user story individuelle par elle-même - par exemple, une user story qui dit que nous récupérons tous les clients lorsque nous accédons à une page Web spécifique peut être un test d'acceptation (simuler la visite du Web page et vérification de la réponse) mais peut également contenir plusieurs tests unitaires (vérifier que les autorisations de sécurité sont vérifiées, vérifier que la connexion à la base de données interroge correctement, vérifier que tout code limitant le nombre de résultats est exécuté correctement) - ce sont tous des "tests unitaires" ce n'est pas un test d'acceptation complet.

Tests de comportement:

Définissez le flux d'une application dans le cas d'une entrée spécifique. Par exemple, "lorsque la connexion ne peut pas être établie, vérifiez que le système réessaye la connexion". Encore une fois, il est peu probable que ce soit un test d'acceptation complet, mais cela vous permet toujours de vérifier quelque chose d'utile.

Ce sont tous à mon avis grâce à une grande expérience de la rédaction de tests; Je n'aime pas me concentrer sur les approches des manuels scolaires - plutôt, se concentrer sur ce qui donne de la valeur à vos tests.

37
Michael

TL; DR: Tant qu'il répond à vos besoins, oui.

Je fais du développement basé sur les tests d'acceptation (ATDD) depuis de nombreuses années maintenant. Cela peut être très réussi. Il y a quelques choses à savoir.

  • Les tests unitaires aident vraiment à appliquer IOC. Sans tests unitaires, il incombe aux développeurs de s'assurer qu'ils répondent aux exigences d'un code bien écrit (dans la mesure où les tests unitaires conduisent à un code bien écrit)
  • Ils peuvent être plus lents et présenter de fausses pannes si vous utilisez des ressources généralement moquées.
  • Le test ne met pas en évidence le problème spécifique comme le feraient les tests unitaires. Vous devez faire plus d'investigation pour corriger les échecs de test.

Maintenant les avantages

  • Meilleure couverture des tests, couvre les points d'intégration.
  • S'assure que le système dans son ensemble répond aux critères d'acceptation, ce qui est tout l'intérêt du développement logiciel.
  • Rend les grands refacteurs beaucoup plus faciles, plus rapides et moins chers.

Comme toujours, c'est à vous de faire l'analyse et de déterminer si cette pratique est appropriée à votre situation. Contrairement à beaucoup de gens, je ne pense pas qu'il existe une bonne réponse idéalisée. Cela dépendra de vos besoins et exigences.

40
dietbuddha

Eh bien, j'ai utilisé l'approche de type TDD pour laisser les tests/exigences faire évoluer la conception du programme. Le problème était que plus de la moitié du temps de développement pour les tests d'écriture/refactorisation

Les tests unitaires fonctionnent mieux lorsque l'interface publique des composants pour lesquels ils sont utilisés ne change pas trop souvent. Cela signifie, lorsque les composants déjà sont bien conçus (par exemple, en suivant les principes SOLID).

Donc, croire qu'une bonne conception "évolue" simplement de "lancer" beaucoup de tests unitaires sur un composant est une erreur. TDD n'est pas un "professeur" pour une bonne conception, il ne peut que contribuer un peu à vérifier que certains aspects de la conception sont bons (en particulier la testabilité).

Lorsque vos besoins changent et que vous devez changer les composants internes d'un composant, cela cassera 90% de vos tests unitaires, vous devez donc les refactoriser très souvent, alors la conception n'était probablement pas si bonne.

Donc, mon conseil est le suivant: pensez à la conception des composants que vous avez créés et à la façon de les rendre plus conformes au principe ouvert/fermé. L'idée de ce dernier est de s'assurer que les fonctionnalités de vos composants peuvent être étendues ultérieurement sans les modifier (et donc de ne pas casser l'API du composant utilisée par vos tests unitaires). Ces composants peuvent (et doivent être) couverts par des tests unitaires, et l'expérience ne devrait pas être aussi douloureuse que vous l'avez décrite.

Lorsque vous ne pouvez pas proposer une telle conception immédiatement, les tests d'acceptation et d'intégration peuvent en effet être un meilleur départ.

EDIT: Parfois, la conception de vos composants peut être fine, mais le la conception de vos tests unitaires peut provoquer des problèmes. Exemple simple: vous souhaitez tester la méthode "MyMethod" de la classe X et écrire

    var x= new X();
    Assert.AreEqual("expected value 1" x.MyMethod("value 1"));
    Assert.AreEqual("expected value 2" x.MyMethod("value 2"));
    // ...
    Assert.AreEqual("expected value 500" x.MyMethod("value 500"));

(supposez que les valeurs ont une certaine signification).

Supposons en outre que dans le code de production, il n'y a qu'un seul appel à X.MyMethod. Maintenant, pour une nouvelle exigence, la méthode "MyMethod" a besoin d'un paramètre supplémentaire (par exemple, quelque chose comme context), qui ne peut pas être omis. Sans tests unitaires, il faudrait refactoriser le code appelant en un seul endroit. Avec les tests unitaires, il faut refactoriser 500 places.

Mais la cause ici n'est pas les tests unitaires eux-mêmes, c'est juste le fait que le même appel à "X.MyMethod" est répété encore et encore, ne suivant pas strictement le principe "Don't Repeat Yourself (DRY). Donc la solution ici est de mettre les données de test et les valeurs attendues associées dans une liste et d'exécuter les appels à "MyMethod" dans une boucle (ou, si l'outil de test prend en charge les soi-disant "tests de lecteur de données", d'utiliser cette fonctionnalité). Cela réduit le nombre d'emplacements à modifier dans les tests unitaires lorsque la signature de la méthode passe à 1 (contre 500).

Dans votre cas réel, la situation pourrait être plus complexe, mais j'espère que vous avez l'idée - lorsque vos tests unitaires utilisent une API de composants pour laquelle vous ne savez pas si elle peut être sujette à changement, assurez-vous de réduire le nombre d'appels à cette API au minimum.

18
Doc Brown

Oui, Bien sur que c'est ça.

Considère ceci:

  • un test unitaire est un petit test ciblé qui exerce un petit morceau de code. Vous en écrivez beaucoup pour obtenir une couverture de code décente, afin que tous (ou la majorité des bits maladroits) soient testés.
  • un test d'intégration est un test large et large qui exerce une grande surface de votre code. Vous en écrivez quelques-uns pour obtenir une couverture de code décente, afin que tous (ou la majorité des bits maladroits) soient testés.

Voyez la différence globale ....

Le problème est lié à la couverture du code, si vous pouvez réaliser un test complet de tout votre code à l'aide de tests d'intégration/d'acceptation, alors il n'y a pas de problème. Votre code est testé. Voilà l'objectif.

Je pense que vous devrez peut-être les mélanger, car chaque projet basé sur TDD nécessitera des tests d'intégration juste pour s'assurer que toutes les unités fonctionnent bien ensemble (je sais par expérience qu'une base de code testée à 100% ne fonctionne pas nécessairement) quand vous les mettez tous ensemble!)

Le problème vient vraiment de la facilité des tests, du débogage des échecs et de leur correction. Certaines personnes trouvent que leurs tests unitaires sont très bons dans ce domaine, ils sont petits et simples et les échecs sont faciles à voir, mais l'inconvénient est que vous devez réorganiser votre code en fonction des outils de test unitaire et en écrire un très grand nombre. Un test d'intégration est plus difficile à écrire pour couvrir beaucoup de code, et vous devrez probablement utiliser des techniques telles que la journalisation pour déboguer les échecs (cependant, je dirais que vous devez le faire de toute façon, vous ne pouvez pas tester les échecs unitaires sur place!).

Quoi qu'il en soit, vous obtenez toujours du code testé, il vous suffit de décider quel mécanisme vous convient le mieux. (J'irais avec un peu de mélange, testerais les algorithmes complexes et intégrerais tester le reste).

9
gbjbaanb

La "victoire" avec TDD, c'est qu'une fois les tests écrits, ils peuvent être automatisés. Le revers de la médaille est qu'il peut consommer une partie importante du temps de développement. Que cela ralentisse réellement l'ensemble du processus est sans objet. L'argument étant que les tests initiaux réduisent le nombre d'erreurs à corriger à la fin du cycle de développement.

C'est là que BDD intervient, car les comportements peuvent être inclus dans les tests unitaires, de sorte que le processus est par définition moins abstrait et plus tangible.

De toute évidence, si un temps infini était disponible, vous feriez autant de tests de différentes variétés que possible. Cependant, le temps est généralement limité et les tests continus ne sont rentables que jusqu'à un certain point.

Tout cela conduit à la conclusion que les tests qui fournissent le plus de valeur devraient être à l'avant-garde du processus. En soi, cela ne favorise pas automatiquement un type de test par rapport à un autre - plus que chaque cas doit être pris en compte.

Si vous écrivez un widget de ligne de commande pour un usage personnel, vous serez principalement intéressé par les tests unitaires. Alors qu'un service Web, par exemple, nécessiterait une quantité substantielle de tests d'intégration/comportementaux.

Alors que la plupart des types de tests se concentrent sur ce que l'on pourrait appeler la "ligne de course", c'est-à-dire tester ce qui est requis par l'entreprise aujourd'hui, les tests unitaires sont excellents pour éliminer les bogues subtils qui pourraient apparaître dans les phases de développement ultérieures. Puisqu'il s'agit d'un avantage qui ne peut pas être facilement mesuré, il est souvent ignoré.

2
Robbie Dee

Je pense que c'est une horrible idée.

Étant donné que les tests d'acceptation et les tests d'intégration touchent des parties plus larges de votre code pour tester une cible spécifique, ils auront besoin de plus refactoring au fil du temps, pas moins. Pire encore, car ils couvrent de larges sections du code, ils augmentent le temps que vous passez à rechercher la cause principale, car vous avez une zone de recherche plus large.

Non, vous devriez généralement écrire plus de tests unitaires, sauf si vous avez une application étrange qui est à 90% d'interface utilisateur ou quelque chose d'autre qui est gênant pour le test unitaire. La douleur que vous rencontrez ne vient pas des tests unitaires, mais du premier développement du test. En règle générale, vous ne devriez consacrer qu'un tiers de votre temps à la plupart des tests de rédaction. Après tout, ils sont là pour vous servir, et non l'inverse.

2
Telastyn

Le dernier avantage des tests unitaires est qu'ils sont plus rapides. J'ai écrit suffisamment de tests d'intégration pour savoir qu'ils peuvent être presque aussi rapides que les tests unitaires.

C'est le point clé, et pas seulement "le dernier avantage". Lorsque le projet prend de plus en plus d'ampleur, vos tests d'acceptation d'intégration deviennent de plus en plus lents. Et ici, je veux dire si lent que vous allez arrêter de les exécuter.

Bien sûr, les tests unitaires deviennent également plus lents, mais ils sont encore plus d'un ordre de grandeur plus rapides. Par exemple, dans mon projet précédent (c ++, quelque 600 kLOC, 4000 tests unitaires et 200 tests d'intégration), il a fallu environ une minute pour exécuter tous et plus de 15 pour exécuter les tests d'intégration. Pour construire et exécuter des tests unitaires pour la pièce à modifier, cela prendrait moins de 30 secondes en moyenne. Lorsque vous pouvez le faire si rapidement, vous voudrez le faire tout le temps.

Juste pour être clair: je ne dis pas de ne pas ajouter de tests d'intégration et d'acceptation, mais il semble que vous ayez mal fait TDD/BDD.

Les tests unitaires sont également bons pour la conception.

Oui, la conception avec la testabilité à l'esprit améliorera la conception.

Le problème était que plus de la moitié du temps de développement pour les tests d'écriture/refactorisation. Donc, à la fin, je ne voulais pas implémenter plus de fonctionnalités car je devrais refactoriser et écrire sur de nombreux tests.

Eh bien, lorsque les exigences changent, vous devez changer le code. Je dirais que vous n'avez pas terminé votre travail si vous n'avez pas écrit de tests unitaires. Mais cela ne signifie pas que vous devriez avoir une couverture à 100% avec des tests unitaires - ce n'est pas l'objectif. Certaines choses (comme l'interface graphique ou l'accès à un fichier, ...) ne sont même pas destinées à être testées à l'unité.

Le résultat est une meilleure qualité de code et une autre couche de tests. Je dirais que ça vaut le coup.


Nous avons également eu plusieurs tests d'acceptation de 1000, et cela prendrait une semaine entière pour tout exécuter.

1
BЈовић