Une base de code bien testée présente un certain nombre d'avantages, mais le test de certains aspects du système se traduit par une base de code résistante à certains types de changement.
Un exemple teste une sortie spécifique - par exemple, du texte ou du HTML. Les tests sont souvent (naïvement?) Écrits pour attendre un bloc de texte particulier en sortie pour certains paramètres d'entrée, ou pour rechercher des sections spécifiques dans un bloc.
Changer le comportement du code, pour répondre à de nouvelles exigences ou parce que les tests d'utilisabilité ont entraîné une modification de l'interface, nécessite également de changer les tests - peut-être même des tests qui ne sont pas spécifiquement des tests unitaires pour le code en cours de modification.
Comment gérez-vous le travail de recherche et de réécriture de ces tests? Et si vous ne pouvez pas simplement "les exécuter tous et laisser le framework les trier"?
Quels autres types de code sous test donnent des tests habituellement fragiles?
la source
Réponses:
Je sais que les gens de TDD détesteront cette réponse, mais une grande partie pour moi est de choisir soigneusement où tester quelque chose.
Si je deviens trop fou avec les tests unitaires dans les niveaux inférieurs, aucun changement significatif ne peut être fait sans modifier les tests unitaires. Si l'interface n'est jamais exposée et n'est pas destinée à être réutilisée en dehors de l'application, il s'agit simplement d'une surcharge inutile par rapport à ce qui aurait pu être un changement rapide autrement.
Inversement, si ce que vous essayez de changer est exposé ou réutilisé, chacun de ces tests que vous allez devoir changer est la preuve de quelque chose que vous pourriez casser ailleurs.
Dans certains projets, cela peut consister à concevoir vos tests à partir du niveau d'acceptation vers le bas plutôt qu'à partir des tests unitaires. et avoir moins de tests unitaires et plus de tests de style d'intégration.
Cela ne signifie pas que vous ne pouvez toujours pas identifier une seule fonctionnalité et un seul code tant que cette fonctionnalité ne répond pas à ses critères d'acceptation. Cela signifie simplement que dans certains cas, vous ne finissez pas par mesurer les critères d'acceptation avec des tests unitaires.
la source
Je viens de terminer une refonte majeure de ma pile SIP, en réécrivant tout le transport TCP. (Il s'agissait d'un quasi-refactor, à une échelle plutôt grande, par rapport à la plupart des refactorings.)
En bref, il existe une sous-classe TIdSipTcpTransport, de TIdSipTransport. Tous les TIdSip Transports partagent une suite de tests commune. En interne à TIdSipTcpTransport se trouvaient un certain nombre de classes - une carte contenant des paires connexion / message initiateur, des clients TCP filetés, un serveur TCP fileté, etc.
Voici ce que j'ai fait:
J'ai donc su ce que je devais encore faire, sous forme de tests commentés (*), et je savais que le nouveau code fonctionnait comme prévu, grâce aux nouveaux tests que j'ai écrits.
(*) Vraiment, vous n'avez pas besoin de les commenter. Ne les lancez pas; 100 tests échouant n'est pas très encourageant. De plus, dans ma configuration particulière, la compilation de moins de tests signifie une boucle de test-écriture-refactorisation plus rapide.
la source
Lorsque les tests sont fragiles, je trouve que c'est généralement parce que je teste la mauvaise chose. Prenons par exemple la sortie HTML. Si vous vérifiez la sortie HTML réelle, votre test sera fragile. Mais vous n'êtes pas intéressé par la sortie réelle, vous voulez savoir si elle transmet les informations qu'elle devrait. Malheureusement, cela nécessite de faire des affirmations sur le contenu du cerveau de l'utilisateur et ne peut donc pas être fait automatiquement.
Vous pouvez:
Le même genre de choses se produit avec SQL. Si vous affirmez le SQL réel, vos classes tentent de vous faire avoir des ennuis. Vous voulez vraiment affirmer les résultats. Par conséquent, j'utilise une base de données de mémoire SQLITE lors de mes tests unitaires pour m'assurer que mon SQL fait réellement ce qu'il est censé faire.
la source
Créez d'abord une NOUVELLE API, qui fait ce que vous voulez que votre comportement de NOUVELLE API soit. S'il arrive que cette nouvelle API porte le même nom qu'une ancienne API, j'ajoute le nom _NEW au nouveau nom de l'API.
int DoSomethingInterestingAPI ();
devient:
int DoSomethingInterestingAPI_NEW (int prend_plus_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (quel que soit_de_fault_mimics_the_old_API); OK - à ce stade - tous vos tests de régression réussissent - en utilisant le nom DoSomethingInterestingAPI ().
SUIVANT, parcourez votre code et remplacez tous les appels par DoSomethingInterestingAPI () par la variante appropriée de DoSomethingInterestingAPI_NEW (). Cela inclut la mise à jour / réécriture de toutes les parties de vos tests de régression qui doivent être modifiées pour utiliser la nouvelle API.
SUIVANT, marquez DoSomethingInterestingAPI_OLD () comme [[obsolète ()]]. Restez dans l'API obsolète aussi longtemps que vous le souhaitez (jusqu'à ce que vous ayez mis à jour en toute sécurité tout le code qui pourrait en dépendre).
Avec cette approche, les échecs dans vos tests de régression sont simplement des bogues dans ce test de régression ou identifient les bogues dans votre code - exactement comme vous le souhaitez. Ce processus par étapes de révision d'une API en créant explicitement les versions _NEW et _OLD de l'API vous permet de faire coexister des bits du nouveau et de l'ancien code pendant un certain temps.
Voici un bon (dur) exemple de cette approche dans la pratique. J'avais la fonction BitSubstring () - où j'avais utilisé l'approche selon laquelle le troisième paramètre serait le COUNT de bits dans la sous-chaîne. Pour être cohérent avec d'autres API et modèles en C ++, je voulais passer en début / fin en tant qu'arguments de la fonction.
https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0
J'ai créé une fonction BitSubstring_NEW avec la nouvelle API et mis à jour tout mon code pour l'utiliser (en ne laissant PLUS D'APPELS à BitSubString). Mais je suis parti dans l'implémentation pour plusieurs versions (mois) - et je l'ai marqué comme obsolète - afin que tout le monde puisse passer à BitSubString_NEW (et à ce moment-là changer l'argument d'un décompte en style de début / fin).
ALORS - lorsque cette transition a été terminée, j'ai fait une autre validation en supprimant BitSubString () et en renommant BitSubString_NEW-> BitSubString () (et j'ai déconseillé le nom BitSubString_NEW).
la source