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.