Je suis un débutant dans le développement de logiciels et j'ai beaucoup lu sur la façon dont les tests unitaires sont cool. Maintenant, j'ai fait mes premiers pas dans un projet où je travaille avec un certain nombre de programmeurs tout aussi inexpérimentés, c'est pourquoi nous avons produit un peu de code spaghetti. J'essaie d'apprendre à utiliser les tests et d'autres techniques pour améliorer la qualité du code, mais l'un de mes collègues débutants dit que les tests rendent les choses plus difficiles. Apparemment, il a effectué des stages dans des équipes où des tests unitaires ont été utilisés.
Il a soutenu que les tests étaient constamment sur son chemin quand il a essayé d'implémenter une nouvelle fonctionnalité. Les tests échoueraient après avoir changé le code. Il a donc dû adapter les tests, ce qui a bien sûr augmenté sa charge de travail.
Mais cela n'a pas de sens pour moi. Je pensais que les tests étaient censés faciliter les choses. Donc, je soupçonne qu'il n'a pas implémenté les fonctionnalités correctement ou que les tests unitaires ont été mal effectués. Je me demande donc: comment puis-je écrire des tests unitaires pour qu'ils n'échouent pas simplement parce qu'une nouvelle fonctionnalité a été implémentée?
En leur faisant tester uniquement ce dont ils parlent, et non pas beaucoup de propriétés non liées qui sont vraies maintenant mais pourraient changer plus tard.
Quelques exemples de mon expérience. Souvent, les systèmes sont censés envoyer des notifications à leurs utilisateurs lorsque certaines choses se produisent. Avec un harnais de test approprié, il est facile de se moquer des messages électroniques et de vérifier qu'ils sortent, d'atteindre le bon destinataire et de dire ce qu'ils sont censés dire. Cependant, ce n'est pas une bonne idée d'affirmer simplement "Lorsque cela se produit même, cet utilisateur reçoit ce texte de message exact". Un tel test échouerait à chaque révision des textes I18N. Il est préférable d'affirmer "Le message contient le nouveau mot de passe de l'utilisateur/Le lien vers la ressource annoncée/le nom de l'utilisateur et un message d'accueil", de sorte que le test continue de fonctionner et ne s'arrête que lorsque la routine ne fait pas, en fait, ce qui est indiqué. emploi.
De même, lorsque vous testez des ID générés automatiquement pour quelque chose, ne supposez jamais que la valeur générée pour le moment sera toujours générée. Même lorsque votre faisceau de test ne change pas, la mise en œuvre de cette fonctionnalité peut changer de sorte que le résultat change tout en respectant son contrat. Encore une fois, vous ne voulez pas affirmer "Le premier utilisateur reçoit un ID AAA", mais plutôt "Le premier utilisateur reçoit un ID composé de lettres et le deuxième utilisateur reçoit un ID également composé de lettres et distinct du premier".
En général, méfiez-vous de tester des choses qui ne sont pas dans le contrat pour la chose que vous testez. Comprendre ce qui est essentiellement vrai sur le comportement d'une unité et ce qui est seulement accidentellement vrai est la clé pour écrire des tests de couverture minimale - et il est également extrêmement utile pour comprendre le système et le maintenir avec succès. C'est une façon dont le développement piloté par les tests améliore les résultats même lorsqu'il ne détecte pas de bogues.
Pour que le logiciel soit hautement testable, il doit être conçu et mis en œuvre en tenant compte des tests. Ceci est complémentaire à la réponse de Killian Foth, où le logiciel est conçu selon un contrat, et le test est effectué pour vérifier ce contrat.
"Concevoir du code pour la testabilité" signifie qu'il faudra apprendre par exemple simulacres, talons, etc. Ce sont différentes façons de créer des abstractions et "coutures" pour permettre la liberté de remplacer les parties logicielles.
(Le mot "coutures" est emprunté au livre Travailler efficacement avec le code hérité par Michael Feathers. Un autre livre recommandé est xUnit Test Patterns: Refactoring Test Code par Gerard Meszaros, qui est lourd mais en vaut vraiment la peine.)
Un apprenant de langue peut avoir à apprendre beaucoup de choses à la fois. Il est donc possible que la "conception de code pour la testabilité" augmente la charge de travail d'un apprenant et ralentisse initialement la progression de l'apprentissage.
La suggestion est d'essayer dans les deux sens. Lorsque vous travaillez en équipe, faites un bon effort pour concevoir la testabilité (et d'autres attributs de qualité), mais chaque personne devrait également avoir un peu de temps de pratique en solo où l'accent est mis sur l'apprentissage de la langue elle-même, ou pour faire quelque chose rapidement (car parfois un tel besoin se fait sentir), ou tout simplement pour faire preuve de créativité et expérimenter quelque chose d'une manière complètement nouvelle et non éprouvée.
Enfin, un programmeur qui a suffisamment d'expérience en programmation et en conception pour la testabilité en viendra à voir favorablement les "tests cassés".
Lorsque le test est interrompu parce que l'implémentation change d'une manière ou d'une autre, si cela se produit lorsque le programmeur ne l'anticipe pas, c'est un moment éclairant où le programmeur a appris quelque chose. Par exemple, le programmeur peut ne pas prévoir que le comportement du code a changé, mais le test dit le contraire. Lorsque cela se produit, le test interrompu a réussi à éviter un bug potentiel.
Oui, il a raison - l'écriture de tests unitaires augmente votre charge de travail, et lorsque vous changez votre code, les tests commencent à échouer et vous devez les mettre à jour et cela augmente encore votre charge de travail.
Cependant ... cela s'applique lorsque vous modifiez du code affecté par les tests, généralement vous écrivez des morceaux de code qui sont testés par certains tests unitaires et vous ne modifiez pas à nouveau ces morceaux de code. Après tout, ils fonctionnent et vous les avez bien conçus, l'ajout de nouvelles fonctionnalités ne vous obligera donc pas à les modifier, et je pense que c'est là que réside le problème. Les codeurs inexpérimentés ont tendance à pirater le code sans suffisamment réfléchir à l'avenir de la base de code. Vous pouvez ajouter une classe avec un champ, puis trouver soudainement que vous devez en ajouter un autre, et le champ d'origine doit changer de type, etc. ce que le code est censé faire et comment il s'intégrera au produit dans son ensemble, et pas seulement à la résolution du problème immédiat. Bien sûr, nous devons encore apporter de tels changements, mais cela arrive tellement rarement avec des développeurs expérimentés.
Une chose que vous pourriez trouver utile dans l'intervalle consiste à utiliser red-green-refactor style de test unitaire. Ceci est similaire à l'utilisation d'un débogueur pour tester, dans la mesure où vous écrivez du code puis l'exécutez dans le débogueur pour voir les erreurs, les corriger et répéter le débogage. Au lieu d'utiliser le débogueur, vous utilisez plutôt un test automatisé. Écrivez le test, exécutez-le sur le code, mettez à jour le code, etc. L'astuce consiste à écrire de petits morceaux de code et à les tester très souvent et vos tests unitaires deviennent plus comme des mini-harnais de test. Je trouve que cette approche convient mieux car elle ressemble beaucoup (conceptuellement) aux tests manuels que vous faites probablement de toute façon.
un de mes collègues débutants dit que les tests rendent les choses plus difficiles.
Je pense que la clé ici est quelles choses ? Il existe de nombreux processus impliqués dans le développement de logiciels. L'écriture de code exécutable n'est que l'une d'entre elles.
Si nous nous concentrons sur l'écriture de code exécutable (/ compilable/etc.), Alors l'écriture de tests rend cela plus difficile, simplement parce qu'il y a plus de code à gérer . Par exemple, si nous effectuons un changement de rupture comme renommer une fonction, nous devrons également mettre à jour le code de test, afin d'atteindre notre objectif de "code exécutable".
Bien sûr, avoir du code exécutable n'est pas l'objectif final: pour une entreprise, l'objectif final est généralement de gagner de l'argent; Pour un développeur, l'objectif final est généralement de réduire le bogue du logiciel ou d'augmenter son ensemble de fonctionnalités/utilité, afin de soutenir le plan de rentabilité de l'entreprise.
Le logiciel est si complexe qu'il est rare de corriger un bogue ou d'introduire une fonctionnalité isolément. Habituellement, les modifications que nous apportons introduisent de nouveaux bogues ou exposent/aggravent des bogues existants. L'ajout de nouvelles fonctionnalités peut casser les anciennes, soit directement (elles ne font pas ce qu'elles faisaient), soit conceptuellement (elles n'ont pas de sens compte tenu de la nouvelle fonctionnalité).
Le but du test est d'essayer de garder objectivement la trace du "bug" et des ensembles de fonctionnalités. En faisant vérifier par une suite de tests que les fonctionnalités existantes fonctionnent, nous nous assurons que notre travail augmente l'ensemble des fonctionnalités, plutôt que d'ajouter de nouvelles choses à la frais de vieux. En faisant vérifier par une suite de tests les entrées/chemins qui sont importants, qui devraient causer des problèmes (cas Edge), ou qui ont déjà causé des problèmes (tests de régression), nous nous assurons que le "bug" est diminuant , plutôt que de corriger de nouveaux bugs au détriment des anciens.
Les tests suivent un schéma courant en génie logiciel: les tests sont revendiqués pour rendre les logiciels meilleurs/plus "agiles"/moins bogués/etc., mais ce n'est pas vraiment le test qui fait cela. Au contraire, de bons développeurs rendent les logiciels meilleurs/plus "agiles"/moins bogués/etc. et les tests sont quelque chose que les bons développeurs ont tendance à faire .
En d'autres termes, effectuer un rituel comme un test unitaire pour lui-même ne améliorera votre code. Pourtant, comprendre pourquoi beaucoup de gens font des tests unitaires feront de vous un meilleur développeur , et être un meilleur développeur améliorera votre code, qu'il ait ou non des tests unitaires .
Malheureusement, nous ne pouvons pas git pull
ces compétences les unes des autres. Cela prend du temps et des efforts, et chaque personne doit apprendre ces choses par elle-même. On dirait que votre collègue a été forcé de subir des tests rituels sans comprendre pourquoi certaines choses sont faites.
Je recommanderais de jouer avec ces idées dans certains projets de jouets. Par exemple, si vous voyez une explication comme "l'injection de dépendance est nécessaire pour des situations comme ..." je dirais essayer de faire un exemple jouet de cette situation, voir quels problèmes vous rencontrez, essayer de les résoudre, et puis essayez de voir en quoi consiste l'injection de dépendances. De cette façon, vous saurez quand quelque chose est approprié, vous saurez quand ce n'est pas le cas, et vous pourrez expliquer et discuter des différentes approches d'un problème (sans simplement réciter ce que les autres prétendent).
Il a soutenu que les tests étaient constamment sur son chemin quand il a essayé d'implémenter une nouvelle fonctionnalité. Les tests échoueraient après avoir changé le code. Il a donc dû adapter les tests, ce qui a bien sûr augmenté sa charge de travail.
C'est la première façon dont les tests aident à implémenter de nouvelles fonctionnalités.
Les tests échoueraient après avoir changé le code.
Le code était nul. Peut-être que le code de test était nul. Plus probablement, son code était nul. De toute façon, le code était nul.
Disons qu'il y a environ 95% de chances que ce soit son code qui soit nul, 2% de chances que le code de test suce exprès ("nous devons interdire le cas A parce que bien qu'il soit généralement parfaitement bien, cela cause des problèmes avec une configuration client héritée étrange B donc nous testons pour nous assurer que personne ne le frappe ") et 3% de chances que le code de test suce.
Maintenant, c'est très généreux, mais même si les taux atteignaient 99,99%, cela ne serait pas mauvais pour les compétences ou le savoir-faire de cette personne. (Tenez compte de la fréquence à laquelle vous rencontrez une erreur de compilation; c'est du code qui est trop mauvais pour même exécuter les tests, mais votre travail global ce jour-là a peut-être été fantastique, tout comme le produit final lorsque vous avez corrigé la source de ces erreurs de compilation). En effet, ce n'est pas vraiment généreux de toucher des pourcentages ici, le code de test a tendance à être le moins souvent à blâmer simplement parce qu'il est changé moins souvent, donc il a moins de chances d'acquérir de nouveaux bogues. Et rappelez-vous que nous parlons du cas d'une défaillance identifiée, et non de la fréquence à laquelle ces défaillances se produisent.
Quoi qu'il en soit, avec ces pourcentages, il y a 98% de chances que quelque chose soit incorrect qui doive être corrigé, 2% de chances que quelque chose soit incorrect en dehors de votre contrôle qui doit être pris en compte et 100% de chances que vous deviez changer quelque chose.
Les 3% de chances que le test soit incorrect se décomposent en plusieurs possibilités:
En tant que tel, même lorsque le test est à blâmer, il peut ne pas être nécessairement "faux" autant que dépassé, parfois.
Dans ces 3% des cas, la gestion des tests vous a coûté un travail supplémentaire que vous n'auriez pas eu si vous n'aviez pas utilisé de tests. Mais une fois que ce travail est fait, il a ajouté de l'aide à 97% du temps lorsque quelque chose d'autre est à blâmer.
Et dans les 97% des fois, le test vous a amené à faire plus de travail à droite alors mais le résultat est que le code non-test n'est plus nul (95% du temps) ou traite de l'amende externe hors de votre contrôle (2% du temps). Notez que le cas de non-faute de 2% est toujours perçu par l'utilisateur comme "ce logiciel est nul".
Donc, 97% (en réalité, plus que cela) du temps où le test a fait sucer le code moins.
Examinons les alternatives pour que 97% sans que les tests unitaires trouvent les défauts:
Tout programmeur professionnel, et tout amateur qui publie son travail, d'ailleurs, auront vu les 6 premiers de ces cas se produire. Certains d'entre nous ont connu la finale 2. Ce sont les scénarios que nous essayons le plus d'éviter, mais ils se produisent.
Lorsque les éléments 2 à 5 se produisent, l'impact sur notre charge de travail est beaucoup plus important que celui d'un test unitaire ayant échoué. Très souvent, on passe plus de temps à traiter ces questions qu'à développer. Un grand nombre de projets ne sont jamais sortis de l'alpha car ils n'ont jamais dépassé de tels problèmes.
Pour les derniers éléments de la liste, l'impact sur la charge de travail n'est peut-être pas pire.
La très grande majorité du temps où vous échouez à un test unitaire, vous avez été sauvé de l'un des scénarios ci-dessus. Les tests unitaires ne vous sauveront pas de tous, mais ils vous sauveront de beaucoup d'entre eux.
Les tests qui réussissent sont sympas. Ils peuvent vous donner une certaine assurance qu'au moins certains bogues ne sont pas présents dans certains unités, et c'est super.
Les tests qui échouent sont ceux qui ont gagné leur subsistance.
Une chose à noter cependant:
Les tests échoueraient après avoir changé le code.
J'espère qu'il y a eu aussi des tests qui ont échoué avant il avait changé le code. Tous les tests liés à la nouvelle fonctionnalité auraient dû échouer, car la nouvelle fonctionnalité n'était pas encore implémentée.