Courte introduction à cette question. J'ai utilisé maintenant TDD et dernièrement BDD pendant plus d'un an maintenant. J'utilise des techniques telles que la moquerie pour rendre mes tests plus efficaces. Dernièrement, j'ai lancé un projet personnel pour écrire un petit programme de gestion de l'argent pour moi-même. Comme je n'avais pas de code hérité, c'était le projet idéal pour commencer avec TDD. Malheureusement, je n’ai pas autant expérimenté la joie du TDD. Cela m'a même tellement gâché le 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 en ce qui concerne l'écriture / tests de refactorisation. Donc, au final, je ne voulais plus mettre en œuvre de fonctionnalités, car il me faudrait 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 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 bugs, les tests unitaires ne sont pas meilleurs que les tests d'intégration / acceptation. Les tests unitaires sont également bons pour la conception. Depuis que 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 jeté un coup d’œil sur le Web, j’ai découvert qu’il existe des idées très similaires aux miennes mentionnées ici et là . Que penses tu de cette idée?
Modifier
En répondant aux questions, un exemple où la conception était bonne, mais il me fallait une refactorisation considérable pour la prochaine exigence:
Au début, il était nécessaire d'exécuter certaines commandes. J'ai écrit un analyseur de commandes extensible - analysant les commandes à partir d'une sorte d'invite de commande et appelant celle qui convient sur le modèle. Les résultats ont été représentés dans une classe de modèle de vue:
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 prochaine exigence était que chaque commande ait sa propre représentation, 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:
Cela était également utile, car chaque commande a désormais son propre modèle de vue et, par conséquent, son propre aperçu.
Le problème, c’est que l’analyseur de commandes a été modifié pour utiliser une analyse syntaxique des commandes basée sur un jeton et qu’il n’a plus la capacité d’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 à afficher.
Tout ce que je voulais savoir à ce stade-ci, c'est si la nouvelle conception ne répondait à aucune exigence existante. Je n'ai pas eu à changer AUCUN de mes tests d'acceptation. J'ai dû refacturer ou supprimer presque TOUS les tests unitaires, ce qui représentait un travail énorme.
Ce que je voulais montrer ici, c’est une situation courante qui s’est souvent produite au cours du développement. L'ancien ou le nouveau design ne posait pas de problème, ils changeaient naturellement avec les exigences - comment je l'ai compris, c'est l'un des avantages de TDD, que le design évolue.
Conclusion
Merci pour toutes les réponses et discussions. En résumé de cette discussion, j'ai réfléchi à une approche que je testerai avec mon prochain projet.
- Tout d’abord, j’écris tous les tests avant de mettre en œuvre quoi que ce soit comme je l’ai toujours fait.
- Pour les besoins, j’écris d’abord des tests de réception qui testent l’ensemble du programme. Ensuite, j'écris des tests d'intégration pour les composants sur lesquels je dois implémenter l'exigence. S'il existe un composant qui travaille en étroite collaboration avec un autre composant pour implémenter cette exigence, j'écrirais également des tests d'intégration dans lesquels 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 cette classe particulière. Toutes les autres classes ne sont pas testées, à l'exception des tests unitaires.
- Pour les bogues, le processus peut être simplifié. Normalement un bug est causé par un ou deux composants. Dans ce cas, j'écrirais un test d'intégration pour les composants qui testent le bogue. Si cela concernait un algorithme, je n’écrirais qu’un test unitaire. S'il n'est pas facile de détecter le composant où le bogue survient, j'écrirais un test d'acceptation pour localiser le bogue - cela devrait être une exception.
la source
Réponses:
C'est comparer des oranges et des pommes.
Tests d'intégration, tests de réception, 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 expliquer pourquoi j'espère que vous avez besoin d'un mélange de chacun d'entre eux:
Tests d'intégration:
Simplement, vérifiez que les différents composants de votre système s’intègrent correctement, par exemple, vous pouvez peut-être simuler une demande de service Web et vérifier que le résultat est renvoyé. J'utiliserais généralement des données statiques réelles (ish) et des dépendances simulées afin de pouvoir les vérifier de manière cohérente.
Tests d'acceptation:
Un test d'acceptation doit être directement lié à un cas d'utilisation métier. Cela peut être énorme ("les transactions sont soumises correctement") ou minuscule ("le filtre filtre avec succès une liste") - peu importe; ce qui compte, c’est qu’il soit 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 disposons d'un bon manuel de référence de tests sur les user stories à dev et à qa à vérifier.
Tests unitaires:
Pour les petites unités discrètes de fonctionnalités qui peuvent constituer ou non une histoire d'utilisateur individuelle - par exemple, une histoire d'utilisateur 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 frappe sur le 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) - il s’agit de "tests unitaires" ce n'est pas un test d'acceptation complet.
Tests de comportement:
Définissez ce que le flux d'une application devrait être 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 tente à nouveau la connexion". Encore une fois, il est peu probable que ce soit un test d'acceptation complet, mais il vous permet néanmoins de vérifier quelque chose d'utile.
Celles-ci sont toutes, à mon avis, le fruit d'une longue expérience de la rédaction de tests Je n'aime pas me concentrer sur les approches de manuel, mais plutôt sur ce qui donne de la valeur à vos tests.
la source
TL; DR: Tant que cela répond à vos besoins, oui.
Je fais du développement ATDD (Acceptance Test Driven Development) depuis de nombreuses années maintenant. Cela peut être très réussi. Il y a quelques choses à connaître.
Maintenant les avantages
Comme toujours, il vous appartient de faire l'analyse et de déterminer si cette pratique convient à 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.
la source
Les tests unitaires fonctionnent mieux lorsque l'interface publique des composants pour lesquels ils sont utilisés ne change pas trop souvent. Cela signifie que les composants sont déjà bien conçus (par exemple, en suivant les principes SOLID).
Il est donc erroné de croire qu'une bonne conception "évolue" en "lançant" de nombreux tests unitaires sur un composant. TDD n’est pas un "enseignant" pour une bonne conception, il ne peut qu’aider un peu à vérifier que certains aspects de la conception sont bons (notamment la testabilité).
Lorsque vos exigences changent et que vous devez modifier les composants internes d'un composant, ce qui fracturera 90% de vos tests unitaires, vous devrez donc les refactoriser très souvent. La conception n'était donc probablement pas aussi bonne.
Mon conseil est donc le suivant: réfléchissez à la conception des composants que vous avez créés et à la manière dont vous pouvez les rendre davantage en suivant le principe d'ouverture / fermeture. L’idée de ce dernier est de s’assurer que la fonctionnalité de vos composants peut être étendue ultérieurement sans la modifier (et donc ne pas casser l’API du composant utilisée par vos tests unitaires). De tels composants peuvent (et devraient être) couverts par des tests unitaires, et l'expérience ne devrait pas être aussi pénible que celle que vous avez décrite.
Lorsque vous ne pouvez pas concevoir une telle conception immédiatement, les tests d'acceptation et d'intégration peuvent s'avérer un meilleur départ.
EDIT: Parfois, la conception de vos composants peut être correcte, mais la conception de vos tests unitaires peut être source de problèmes . Exemple simple: vous voulez tester la méthode "MyMethod" de la classe X et écrire
(supposons que les valeurs ont un sens).
Supposons en outre que dans le code de production, il n’ya qu’un appel à
X.MyMethod
. Désormais, pour une nouvelle exigence, la méthode "MyMethod" nécessite un paramètre supplémentaire (par exemple, quelque chose du genrecontext
), qui ne peut pas être omis. Sans tests unitaires, il faudrait refactoriser le code d'appel en un seul endroit. Avec les tests unitaires, il faut refactoriser 500 places.Mais la cause ici n’est pas que l’unité se teste elle-même, c’est simplement le fait que le même appel à "X.MyMethod" est répété encore et encore, en ne suivant pas strictement le principe "Ne vous répétez pas (DRY)". La solution Ceci consiste à mettre les données de test et les valeurs attendues associées dans une liste et à exécuter en boucle les appels à "MyMethod" (ou, si l’outil de test prend en charge les "tests de lecteur de données", à utiliser cette fonctionnalité). le nombre de places à modifier dans les tests unitaires lorsque la signature de la méthode passe à 1 (au lieu de 500).
Dans votre cas réel, la situation pourrait être plus complexe, mais j'espère que vous en aurez une idée: lorsque vos tests unitaires utilisent une API de composants pour laquelle vous ne savez pas si elle peut être modifiée, veillez à en réduire le nombre. d'appels à cette API au minimum.
la source
X x= new X(); AssertTrue(x.MyMethod(12,"abc"))
avant d'implémenter réellement la méthode. En utilisant la conception initiale, vous pouvez écrire enclass X{ public bool MyMethod(int p, string q){/*...*/}}
premier et écrire les tests plus tard. Dans les deux cas, vous avez pris la même décision de conception. Si la décision était bonne ou mauvaise, TDD ne vous le dira pas.Oui, Bien sur que c'est ça.
Considère ceci:
Voir la différence globale ....
Il s’agit d’un problème de couverture de code. Si vous pouvez réaliser un test complet de tout votre code à l’aide des tests d’intégration / acceptation, il n’ya pas de problème. Votre code est testé. C'est le but.
Je pense que vous aurez peut-être besoin de les mélanger, car chaque projet basé sur TDD nécessitera des tests d'intégration pour s'assurer que toutes les unités fonctionnent bien ensemble (je sais par expérience qu'une base de code testée à 100% réussie ne fonctionne pas nécessairement quand vous les mettez tous ensemble!)
Le problème réside vraiment dans la facilité des tests, du débogage des défaillances et de leur résolution. Certaines personnes trouvent que leurs tests unitaires sont très efficaces, elles sont petites et simples et les échecs sont faciles à voir, mais le désavantage est que vous devez réorganiser votre code pour l'adapter aux 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 comme la journalisation pour déboguer les échecs (bien que, je dirais que vous devez le faire de toute façon, vous ne pouvez pas effectuer d'échec de test unitaire. quand 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. (Je ferais un peu du mixage, testera les algorithmes complexes et intégrera le reste).
la source
Je pense que c'est une idée horrible.
Comme les tests d'acceptation et les tests d'intégration touchent des portions plus larges de votre code pour tester une cible spécifique, ils auront besoin de plus de refactorisation au fil du temps, pas moins. Pire encore, dans la mesure où ils couvrent de larges sections du code, ils augmentent le temps que vous passez à rechercher la cause du problème car vous avez une zone de recherche plus étendue.
Non, vous devriez généralement écrire plus de tests unitaires, sauf si vous avez une application étrange qui est 90% UI ou quelque chose d'autre qui est difficile à tester. La douleur que vous rencontrez ne provient pas des tests unitaires, mais du développement du premier test. En règle générale, vous ne devriez consacrer qu’un tiers de votre temps à la plupart des tests d’écriture. Après tout, ils sont là pour vous servir, pas l'inverse.
la source
Le "gain" avec TDD est qu’une fois les tests écrits, ils peuvent être automatisés. Le revers de la médaille est qu'il peut consommer une grande partie du temps de développement. Que cela ralentisse réellement l'ensemble du processus est discutable. 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 concret.
De toute évidence, si vous disposiez d’un temps infini, vous feriez autant de tests que possible de différentes variétés. Cependant, le temps est généralement limité et les tests continus ne sont rentables que jusqu'à un certain point.
Tout cela nous amène à la conclusion que les tests offrant le plus de valeur devraient être au premier plan du processus. Cela en soi ne favorise pas automatiquement un type de test par rapport à un autre - plus que chaque cas doit être examiné selon ses mérites.
Si vous écrivez un widget de ligne de commande pour un usage personnel, les tests unitaires vous intéresseront principalement. Alors qu’un service Web, par exemple, nécessiterait une quantité importante 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 ce qui est demandé par l’entreprise, les tests unitaires sont excellents pour éliminer les bugs subtils qui pourraient faire surface au cours des phases de développement ultérieures. Comme il s'agit d'un avantage difficilement mesurable, il est souvent négligé.
la source
C'est le point clé, et pas seulement "le dernier avantage". Lorsque le projet devient de plus en plus important, vos tests d'acceptation d'intégration deviennent de plus en plus lents. Et ici, je veux dire si lentement que vous allez arrêter de les exécuter.
Bien sûr, les tests unitaires sont également de plus en plus lents, mais ils sont toujours plus rapides. Par exemple, dans mon projet précédent (c ++, quelques 600 kLOC, 4000 tests unitaires et 200 tests d’intégration), il fallait environ une minute pour exécuter tous les tests et plus de 15 pour exécuter les tests d’intégration. Construire et exécuter des tests unitaires pour la pièce à modifier prendrait en moyenne moins de 30 secondes. Lorsque vous pouvez le faire si vite, vous aurez envie de le faire tout le temps.
Soyons clairs: je ne dis pas qu'il ne faut pas ajouter de tests d'intégration et d'acceptation, mais il semblerait que vous ayez mal interprété TDD / BDD.
Oui, concevoir en gardant à l’esprit la testabilité améliorera la conception.
Eh bien, lorsque les exigences changent, vous devez changer le code. Je vous dirais que vous n’avez pas terminé votre travail si vous n’écrivez pas de tests unitaires. Mais cela ne signifie pas que vous devriez avoir une couverture de 100% avec les 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 passé plusieurs milliers de tests d’acceptation, et il faudrait une semaine entière pour tout exécuter.
la source