Pourquoi la plupart des tests unitaires sont-ils des déchets?

By | août 9, 2019

Formation gratuite Python

Je ne me souviens pas comment j’ai rencontré cet article de James O Coplien. Cependant, j'ai été immédiatement impressionné par la pensée et l'expérience de ce document.
Quels que soient vos points de vue sur les tests unitaires par rapport aux autres types de tests automatisés, cet article est important à lire.
Si votre première réaction au titre est de la colère, prenez une profonde respiration, essayez de garder l’esprit ouvert et LIS ce que Cope a à dire.
Je vais réserver mes propres réactions à ce sujet dans un prochain article, car je ne veux pas colorer vos points de vue avant de le lire.
Je poste l'article complet sans autre changement que le formatage.


Pourquoi la plupart des tests unitaires sont-ils des déchets?

Par James O Coplien

1.1 Dans les temps modernes

Le test unitaire était un élément essentiel des journées Fortran, quand une fonction était une fonction et était parfois digne d'un test fonctionnel. Ordinateurs calculés, et les fonctions et procédures unités de calcul représentées. À cette époque, le processus de conception dominant consistait en une fonctionnalité externe complexe composée de morceaux plus petits, qui à leur tour orchestraient des morceaux encore plus petits, et ainsi de suite, jusqu'au niveau de primitives bien comprises. Chaque couche supportait les couches au-dessus de celle-ci. En fait, vous aviez de bonnes chances de pouvoir relier la fonctionnalité des éléments du bas, appelés fonctions et procédures, aux exigences qui les ont provoquées à l’interface humaine. Il était à espérer qu’un bon concepteur puisse comprendre les objectifs commerciaux d’une fonction donnée. Et il était possible, du moins dans un code bien structuré, de raisonner sur l'arbre appelant. Vous pouvez simuler mentalement l'exécution de code lors d'une révision de code.

L'orientation des objets a lentement pris d'assaut le monde et a bouleversé le monde du design. Premièrement, les unités de conception sont passées des choses qui ont été calculées à des composites hétérogènes de petite taille, appelés objets, qui combinent plusieurs artefacts de programmation, y compris des fonctions et des données, dans un même wrapper. Le paradigme de l'objet utilisé Des classes pour envelopper plusieurs fonctions avec les spécifications des données globales à ces fonctions. La classe est devenue un emporte-pièce à partir duquel objets ont été créés au moment de l'exécution. Dans un contexte informatique donné, la fonction exacte à appeler est déterminée au moment de l'exécution et ne peut pas être déduite du code source comme cela pourrait être le cas dans FORTRAN. Cela rendait impossible de raisonner sur le comportement du code à l'exécution à la seule inspection. Vous deviez exécuter le programme pour avoir la moindre idée de ce qui se passait.
passe.

Alors, les tests sont devenus dans encore. Et c’était des tests unitaires avec vengeance. La communauté d'objets avait découvert l'intérêt d'un retour rapide d'informations, propulsé par la vitesse croissante des machines et par l'augmentation du nombre d'ordinateurs personnels. La conception est devenue beaucoup plus centrée sur les données car les objets ont été façonnés davantage par leur structure de données que par les propriétés de leurs méthodes. L’absence de structure d’appel explicite rend difficile de placer une exécution de fonction unique dans le contexte de son exécution. Le peu de chance qu’il y ait eu de le faire a été enlevé par le polymorphisme. Les tests d'intégration étaient donc terminés. les tests unitaires étaient en cours. Les tests système étaient toujours quelque part à l’arrière-plan, mais semblaient devenir le problème de quelqu'un d’autre ou, plus dangereux encore, dirigés par les mêmes personnes qui avaient écrit le code comme une version adulte du test unitaire.

Les classes sont devenues les unités d'analyse et, dans une certaine mesure, de conception. Les cartes CRC (représentant couramment les classes, les responsabilités et les collaborateurs) étaient une technique de conception populaire dans laquelle chaque classe était représentée par une personne. L'orientation des objets est devenue synonyme de conception anthropomorphique. Les classes sont en outre devenues des unités d’administration, de conception et de programmation, et leur nature anthropomorphique a donné au maître de chaque classe le désir de la tester. Et parce que peu de méthodes de classes avaient la même contextualisation qu'une fonction FORTRAN, les programmeurs devaient fournir le contexte avant d’exercer une méthode (rappelez-vous que nous ne testons pas les classes et nous ne testons même pas les objets – l’unité de test fonctionnel est une méthode). Les tests unitaires ont permis aux conducteurs de suivre les méthodes à leur rythme. Mock fournissait le contexte de l'état de l'environnement et des autres méthodes sur lesquelles reposait la méthode testée. De plus, les environnements de test étaient dotés d'installations permettant de placer chaque objet dans le bon état en préparation du test.

1.2 Le remède est pire que le mal

Les tests unitaires ne sont bien sûr pas un problème pour la programmation orientée objet, mais la combinaison de l'orientation orientée objet, du développement logiciel agile, et de la montée en puissance des outils et de la puissance de calcul l'ont rendue de riguer. En tant que consultant, je reçois souvent des questions sur les tests unitaires, y compris celui-ci d'un de mes clients récents, Richard Jacobs de Sogeti (Sogeti Nederland B.V.):

Ma deuxième question concerne les tests unitaires. Si je me souviens bien, vous avez dit que les tests unitaires sont une perte. Tout d'abord, j'ai été surpris par cela. Aujourd'hui, cependant, mon équipe m'a dit que les tests sont plus complexes que le code réel. (Cette équipe n'est pas la première équipe qui a écrit le code et les tests unitaires. Certains tests unitaires les prennent donc par surprise. Cette équipe actuelle est plus senior et plus disciplinée.) À mon avis, maintenant c'est du gaspillage… Quand je programmais quotidiennement En principe, j’ai fabriqué du code à des fins de testabilité, mais j’ai à peine écrit des tests unitaires. Cependant, j'étais réputé pour la qualité de mon code et mon logiciel presque sans bug. J'aime enquêter POURQUOI cela a-t-il fonctionné pour moi?

Vous vous souviendrez de votre formation dans une école de métiers que vous pouvez modéliser n’importe quel programme comme une bande de Turing. Ce que le programme peut faire est en quelque sorte lié au nombre de bits de cette bande au début de l’exécution. Si vous souhaitez tester minutieusement ce programme, vous devez disposer d’un test avec au moins la même quantité d’informations: c’est-à-dire une autre bande de Turing d’au moins le même nombre de bits.

En pratique, les aléas du langage de programmation rendent difficile la réalisation de ce type de compacité d'expression dans un test Achevée lors des tests, le nombre de lignes de code dans les tests unitaires devrait être supérieur d'un ordre de grandeur à celui de l'unité testée. Peu de développeurs admettent qu’ils ne font que des tests aléatoires ou partiels et beaucoup vous diront qu’ils font des tests complets pour une vision supposée de Achevée. Ces visions incluent des notions telles que: «Chaque ligne de code a été atteinte», ce qui, du point de vue de la théorie de l'informatique, est un pur non-sens en termes de savoir si le code fait ce qu'il devrait. Nous aborderons ce problème plus en détail ci-dessous. Mais la plupart des programmeurs considèrent les tests unitaires de cette façon, ce qui signifie qu’il est voué à l’échec dès le début.

☞ Soyez modéré sur ce que vos tests unitaires peuvent réaliser, à moins que vous ne disposiez d'un système d'exigences extrinsèques pour l'unité testée. Il est peu probable que les tests unitaires testent plus d'un billion de fonctionnalités d'une méthode donnée dans un cycle de test raisonnable. Passer à autre chose.

(Trillion n'est pas utilisé rhétoriquement ici, mais est basé sur les différents états possibles étant donné que la taille moyenne de l'objet est de quatre mots et que l'estimation prudente selon laquelle vous utilisez des mots de 16 bits).

1.3 Tests pour leur propre intérêt et tests conçus

J'avais un client en Europe du Nord où les développeurs devaient avoir une couverture de code de 40% pour la maturité logicielle de niveau 1, de 60% pour le niveau 2 et de 80% pour le niveau 3, alors que certains aspiraient à une couverture de code à 100%. Aucun problème! On pourrait penser qu’une procédure assez complexe avec des branches et des boucles aurait constitué un défi, mais c’est juste une question de diviser et impera. Les grandes fonctions pour lesquelles une couverture de 80% était impossible ont été divisées en plusieurs petites fonctions pour lesquelles la couverture de 80% était triviale. Cela a élevé la mesure globale de maturité de ses équipes en un an, car vous obtiendrez certainement ce que vous récompensez. Bien entendu, cela signifie également que les fonctions ne sont plus des algorithmes encapsulés. Il n'était plus possible de raisonner sur le contexte d'exécution d'une ligne de code en termes de lignes qui le précèdent et le suivent en cours d'exécution, car ces lignes de code ne sont plus adjacentes à celle qui vous préoccupe. Cette transition de séquence a maintenant eu lieu à travers un appel de fonction polymorphe – un GOTO hyper-galactique. Mais si tout ce qui vous préoccupe est la couverture des succursales, cela n’a aucune importance.

☞ Si vos testeurs divisent des fonctions pour prendre en charge le processus de test, vous détruisez l’architecture de votre système et la compréhension du code. Testez à un niveau de granularité plus grossier.

Et ce n’est que du code de masse. Vous pouvez réduire la masse de code d'application, mais ce code contient des boucles qui «trichent» la théorie de l'information en encapsulant de nombreuses lignes de code dans un petit espace. Cela signifie que les tests doivent être au moins aussi complexe de calcul comme code. Vous avez non seulement de nombreux tests, mais des tests très longs. Tester toute combinaison raisonnable d'indices de boucle dans une fonction simple peut prendre des siècles.

Une attaque par force brute sur ce problème consiste à exécuter des tests en continu. Les gens confondent les tests automatisés avec les tests unitaires: à tel point que lorsque je critique les tests unitaires, les gens me reprochent d'avoir critiqué l'automatisation.

☞ Si vous écrivez un test couvrant autant de possibilités que possible, vous pouvez dédier un bâti de machines à l'exécution des tests 24 heures sur 24, 7 jours sur 7, en effectuant le suivi de l'enregistrement le plus récent.

Rappelez-vous, cependant, que la merde automatisée est toujours la merde. Et ceux d'entre vous qui ont un programme Lean coroporate pourraient noter que les fondements du système de production Toyota, qui étaient les fondements de Scrum, étaient nettement opposés à l'automatisation des tâches intellectuelles (http://www.computer.org/portal/ web / buildyourcareer / Agile-Careers / – / blogs / autonomisation). C’est plus puissant de garder l’être humain au courant, comme il est plus évident dans les tests exploratoires. Si vous voulez automatiser, automatisez quelque chose de valeur. Et vous devriez automatiser les choses banales. Vous obtiendrez probablement un meilleur retour sur votre investissement en automatisant les tests d'intégration, les tests de régression des bogues et les tests système plutôt qu'en automatisant les tests unitaires.

Une approche plus intelligente permettrait de réduire la masse du code de test grâce à une conception de test formelle: c’est-à-dire effectuer une vérification formelle des conditions aux limites, effectuer davantage de tests dans une boîte blanche, etc. Cela nécessite que l'unité testée soit conçu pour la testabilité. C’est comme cela que les ingénieurs en matériel le font: les concepteurs fournissent des «points de test» capables de lire les valeurs sur la broche J-Tag d’une puce pour accéder aux valeurs de signal internes de la puce, ce qui revient à accéder à des valeurs entre des calculs intermédiaires dans une unité de calcul. Je préconise de le faire au niveau du système, là où le test devrait être l’objet; Je n'ai jamais vu personne atteindre cet objectif au niveau de l'unité. Sans ces crochets, vous êtes limité aux tests unitaires en boîte noire.

Je pourrais croire en une conception formalisée des tests unitaires si le comportement peut être formalisé, c'est-à-dire s'il existe un oracle absolu et formel d'exactitude à partir duquel le test peut être dérivé. Plus à ce sujet ci-dessous. Sinon, ce ne sont que les suppositions du programmeur.

☞ Les tests doivent être conçus avec le plus grand soin. Les hommes d’affaires, plutôt que les programmeurs, devraient concevoir la plupart des tests fonctionnels. Les tests unitaires doivent être limités à ceux qui peuvent être bloqués par rapport à certains critères de réussite «tiers».

1.4 La conviction que les tests sont plus intelligents que les télégraphes codés Une peur latente ou un mauvais processus

Les programmeurs ont la conviction tacite qu'ils peuvent penser plus clairement (ou mieux deviner) lors de la rédaction de tests lors de l'écriture de code, ou qu'il y a plus d'informations dans un test que dans le code. C'est juste un non-sens formel. La perspective psychologique est instructive ici, et c’est important parce que c’est la majorité des comportements, plutôt que toute propriété informatique, qui déterminent le comportement des développeurs.

Si vos codeurs ont plus de lignes de tests unitaires que de code, cela signifie probablement plusieurs choses. Ils peuvent être paranoïaques à propos de la correction; la paranoïa éloigne la pensée claire et l'innovation qui sont au cœur de la qualité. Ils manquent peut-être d'outils mentaux analytiques ou d'une discipline de pensée et ils veulent que la machine réfléchisse pour eux. Les machines sont capables de répéter des tâches mécaniques, mais la conception des tests nécessite toujours une réflexion approfondie. Il se peut également que votre processus ne vous permette pas une intégration fréquente, à cause d'une mauvaise conception ou de mauvais outils. Les programmeurs font de leur mieux pour compenser en créant des tests dans un environnement dans lequel ils ont un certain contrôle sur leur propre destin.

☞ Si vous avez une masse de test unitaire importante, évaluez les boucles de rétroaction de votre processus de développement. Intégrez le code plus fréquemment; réduire les temps de construction et d'intégration; couper les tests unitaires et aller plus pour les tests d'intégration.

Le problème peut également se trouver à l’autre bout: les développeurs n’ont pas les compétences requises en matière de conception, ni le processus n’encourage la réflexion architecturale et la conception consciencieuse. Peut-être que les exigences sont si mauvaises que les développeurs ne sauraient pas quoi tester s’ils le devaient, alors ils font leur meilleure estimation. La recherche en génie logiciel a montré que la suppression des bogues était le moyen le plus rentable de passer de l'analyse à la conception, dans la conception même et dans les disciplines de la programmation. Il est beaucoup plus facile d’éviter de créer des bogues que de les supprimer.

☞ Si vous avez des tests unitaires complets, mais que vous avez toujours un taux d’échec élevé lors des tests système ou une qualité médiocre sur le terrain, n’assumez pas automatiquement les tests (tests unitaires ou tests système). Examinez attentivement vos exigences et votre schéma de conception, ainsi que leur lien avec les tests d'intégration et les tests de système.

Mais soyons clairs: il y aura toujours des bugs. Les tests ne vont pas disparaître.

1.5 Les tests à faible risque ont un bénéfice faible (voire potentiellement négatif)

J'ai dit à mon client que je devinais que beaucoup de leurs tests pourraient être tautologiques. Peut-être que toute une fonction ne fait que mettre X à 5, et je parierai qu’elle sera testée pour voir si la valeur de X est 5 après son exécution. Encore une fois, un bon test repose sur une réflexion approfondie et sur les principes de base de la gestion des risques. La gestion des risques repose sur des statistiques et sur la théorie de l'information. Si les testeurs (ou au moins le responsable des tests) n’ont pas au moins les compétences rudimentaires dans ce domaine, vous ferez probablement beaucoup de tests inutiles.

Disséquons un exemple trivial. Le but du test est de créer des informations sur votre programme. (Les tests n'améliorent pas la qualité; la programmation et la conception ne les améliorent pas. Les tests fournissent simplement les informations qui manquaient à l'équipe pour une conception et une mise en œuvre correctes.) La plupart des programmeurs souhaitent «entendre» les «informations» indiquant que leur composant de programme fonctionne. Ainsi, quand ils ont écrit leur première fonction pour ce projet il y a trois ans, ils ont écrit un test unitaire pour cela. Le test n'a jamais échoué. La question est la suivante: combien d’informations se trouvent dans ce test? Autrement dit, si «1» est la réussite d'un test et que «0» est l'échec d'un test, combien d'informations se trouvent dans cette chaîne de résultats de test:

1111111111111111111111111111111111

Il existe plusieurs réponses possibles selon le formalisme que vous appliquez, mais la plupart des réponses sont fausses. La réponse naïve est 32, mais ce sont les bits de Les données, pas de information. Vous pourriez être un théoricien de l'information et dire que le nombre de bits d'information dans une chaîne binaire homogène est le journal binaire de la longueur de la chaîne, qui dans ce cas est 5. Cependant, ce n'est pas ce que je veux savoir: À la fin, je veux savoir combien d’informations je reçois d’une seule fois de ce test. L'information est basée sur la probabilité. Si la probabilité de réussite du test est de 100%, il y a non information – par définition, de la théorie de l'information. Il n'y a presque aucune information dans les 1 de la chaîne ci-dessus. (Si la chaîne était infiniment longue, il y aurait exactement zéro bit d'information dans chaque exécution de test.)

Maintenant, combien de bits d'information dans cette chaîne de tests s'exécute?

1011011000110101101000110101101

La réponse est… beaucoup plus. Probablement 32. Cela signifie qu’il y a beaucoup plus d’informations dans chaque test. Si nous ne pouvons pas prédire au départ si un test réussira ou échouera, alors chaque test contient une information complète, et vous ne pouvez pas vous en sortir mieux. Vous voyez, les développeurs aiment garder les tests qui passent parce que c’est bon pour leur ego et leur niveau de confort. Mais l'information vient de échoué tests. (Bien sûr, nous pouvons prendre l'autre extrême:

00000000000000000000000000000000

où il n’existe vraiment aucune information, du moins sur le processus d’amélioration de la qualité).

☞ Si vous souhaitez réduire votre masse de test, vous devez avant tout examiner les tests qui n'ont jamais échoué depuis un an et envisager de les jeter. Ils ne produisent aucune information pour vous – ou du moins très peu d'informations. La valeur des informations qu’ils produisent ne vaut peut-être pas la dépense liée à la maintenance et à l’exécution des tests. Il s'agit du premier ensemble de tests à jeter, qu'il s'agisse de tests unitaires, de tests d'intégration ou de tests système.

Un autre de mes clients a également subi trop de tests unitaires. Je leur ai fait remarquer que cela réduirait leur vitesse, car tout changement dans une fonction devrait nécessiter un changement coordonné du test. Ils m'ont informé qu'ils avaient rédigé leurs tests de manière à ne pas avoir à les modifier lorsque la fonctionnalité avait changé. Bien sûr, cela signifie que les tests n’ont pas été testés, donc peu importe la valeur de leurs tests.

Ne sous-estimez pas l’intelligence de votre peuple, mais ne sous-estimez pas la stupidité collective de nombreuses personnes travaillant ensemble dans un domaine complexe. Vous pensez probablement que vous ne feriez jamais ce que l'équipe ci-dessus a fait, mais je trouve toujours de plus en plus de choses comme celle-là qui défient presque toute croyance. Il est probable que vous ayez certains de ces squelettes dans votre placard. Chassez-les, riez bien, réparez-les et continuez.

☞ Si vous avez des tests de ce type, c’est le deuxième ensemble de tests à jeter.

Le troisième teste pour jeter les tautologiques. J'en vois plus que ce que vous pouvez imaginer – en particulier dans les magasins qui suivent ce qu'ils appellent un développement piloté par les tests. (En fait, le test de non-nullité lors de l'entrée dans une méthode n'est pas un test tautologique – et peut être très informatif. Cependant, comme avec la plupart des tests unitaires, il vaut mieux en faire une affirmation que de pimenter votre framework de test avec de tels contrôles. Plus à ce sujet ci-dessous.)

Dans la plupart des entreprises, les seuls tests ayant une valeur commerciale sont ceux qui sont dérivés des besoins de l'entreprise. La plupart des tests unitaires sont dérivés des fantasmes des programmeurs sur le fonctionnement de la fonction. cela n'a aucune valeur prouvable. Dans les années 70 et 80, il existait des méthodologies basées sur la traçabilité qui tentaient de réduire les exigences système au niveau de l'unité. En général, c’est un problème très complexe (à moins que vous ne procédiez à une décomposition procédurale pure), je suis donc très sceptique à l’égard de quiconque affirmant pouvoir le faire. Donc, une question à poser à propos de chaque test est: Si ce test échoue, quelle exigence métier est compromise? La plupart du temps, la réponse est «Je ne sais pas». Si vous ne connaissez pas la valeur du test, le test pourrait théoriquement avoir une valeur commerciale nulle. Le test Est-ce que avoir un coût: maintenance, temps de calcul, administration, etc. Cela signifie que le test pourrait avoir valeur négative nette. C'est la quatrième catégorie de tests à supprimer. Ce sont des tests qui, bien qu’ils puissent même faire une certaine vérification, ne font pas de validation.

☞ Si vous ne pouvez pas dire en quoi un échec de test unitaire contribue au risque du produit, vous devez évaluer s'il faut jeter le test. En l'absence de critères de correction formels, tels que les tests exploratoires et les techniques de Monte Carlo, il existe de meilleures techniques pour remédier aux défauts de qualité. (Celles-ci sont excellentes et je les considère comme appartenant à une catégorie distincte de celle à laquelle j’adresse ici.) Ne pas utiliser de tests unitaires pour une telle validation.

Notez qu'il existe certaines unités pour lesquelles il existe une réponse claire à la question de la valeur commerciale. Un tel ensemble de tests est constitué par les tests de régression; cependant, ceux-ci sont rarement écrits au niveau de l'unité mais plutôt au niveau du système. Nous savons quel bogue reviendra si un test de régression échoue – par construction. De plus, certains systèmes ont des algorithmes clés, tels que les algorithmes de routage réseau, qui peuvent être testés sur une seule API. Comme je l'ai dit plus haut, il existe un oracle formel permettant de déduire les tests de telles API. Donc, ces tests unitaires ont une valeur.

☞ Déterminez si le gros de vos tests unitaires doit être celui qui teste les algorithmes de clé pour lesquels il existe un oracle «tiers», plutôt que celui créé par la même équipe qui écrit le code. Le «succès» doit ici refléter un mandat commercial plutôt que, par exemple, l’opinion d’un membre de l’équipe appelé «testeur», dont l’opinion n’est appréciée que parce qu’elle est indépendante. Bien entendu, une perspective d'évaluation indépendante est également importante.

1.6 Les choses complexes sont compliquées

Il existe un dilemme ici, à savoir que dans certains logiciels, la plupart des données de qualité intéressantes se trouvent dans la queue des distributions des résultats de test, et les approches classiques en matière de statistiques vous informent mal. Donc, un test peut passer 99,99% du temps, mais le test qui échoue sur dix mille vous tue. Encore une fois, en empruntant du monde matériel, vous pouvez concevoir pour une probabilité d’échec donnée ou vous pouvez le faire. analyse du pire des cas (WCA) pour réduire la probabilité d'échec à des niveaux arbitrairement bas. Les utilisateurs de matériel utilisent généralement WCA lors de la conception de systèmes asynchrones pour se protéger contre les «problèmes» d’arrivée de signaux qui sortent des paramètres de conception une fois sur 100 millions. En matériel, on dirait qu'un tel module a un taux de FIT de 10 à 10 défaillances sur un billion.

Le client que j’ai mentionné au début de cet article s’interrogeait sur la raison pour laquelle les tests ne fonctionnaient pas dans son équipe, car ils avaient déjà travaillé pour lui auparavant. Je lui ai envoyé une version antérieure de ce document et il a répondu:

C'est un plaisir de le lire tout en expliquant pourquoi les choses ont fonctionné pour moi (et le reste de l'équipe). Comme vous le savez peut-être, je suis un ingénieur en avionique dont la carrière a débuté en tant que développeur de logiciels embarqués avec un pied dans le développement de matériel. C'est comme ça que j'ai commencé à tester mon logiciel, avec un état d'esprit matériel. (C’était une équipe de quatre hommes: 3 ingénieurs électriciens de l’Université de Delft (dont une spécialisée en avionique) et un ingénieur logiciel (L’Université de La Haye). Nous avons fait preuve d’une grande discipline lorsque nous travaillions sur les systèmes de sécurité pour les banques, les pénitenciers et les pompiers. postes de police, services d’urgence, usines de produits chimiques, etc. tout le temps.)

Sur la base d’hypothèses raisonnables, vous pouvez effectuer un contrôle de performance matérielle en grande partie du fait que les relations de cause à effet sont faciles à déceler: nous pouvons examiner le câblage afin de déterminer les causes de la modification de l’état d’un élément de la mémoire. Les états d'une machine de Von Neumann changent en tant qu'effet secondaire de l'exécution d'une fonction et il est en général impossible de rechercher la cause d'un changement d'état donné, ou même si un état donné est accessible. L'orientation objet aggrave la situation. Il est impossible de savoir, pour une utilisation donnée d'une valeur d'état dans un programme, quelle instruction a modifié cet état en dernier.

La plupart des programmeurs pensent que la couverture de la ligne source, ou du moins la couverture des branches, est suffisante. Non. Du point de vue de la théorie informatique, la couverture la plus défavorable signifie l’enquête sur tous les problèmes possibles. combinaison de machine séquences de langage, en s'assurant que chaque instruction est atteinte et en prouvant que vous avez reproduit toutes les configurations possibles de bits de données du programme pour chaque valeur du compteur de programme. (Il est insuffisant de reproduire l'espace d'état uniquement pour le module ou la classe contenant la fonction ou la méthode testée: généralement, toute modification peut apparaître n'importe où ailleurs dans un programme et nécessite que le programme complet puisse être à nouveau testé. Pour une preuve formelle , voir le document: Perry et Kaiser, «Tests adéquats et programmation orientée objet», Journal de programmation orientée objet 2 (5), janvier 1990, p. 13). Pour un petit programme, nous sommes déjà dans un inventaire de test bien au-delà du nombre de molécules dans l'univers. (Ma définition de la couverture de code est le pourcentage de toutes les paires possibles, Compteur de programme, Etat du système que votre suite de tests reproduit; Toute autre chose est une heuristique, et vous aurez probablement du mal à trouver la moindre justification.) La plupart des diplômés en informatique de premier cycle reconnaîtront le problème de l’arrêt dans la plupart des variantes de cet exercice et sauront que c’est impossible.

1.7 Moins, c'est plus, ou: vous n'êtes pas schizophrène

Il ya un autre casse-tête ici, en particulier en ce qui concerne la question initiale de mon client. Le testeur naïf essaiera de manipuler les données des queues en conservant tous les tests, voire en ajoutant d'autres; cela conduit exactement à la situation dans laquelle se trouve mon client, avec plus de complexité (ou de masse de code ou de choix de votre mesure préférée) dans les tests que dans le code. Les classes qu'il testait sont du code. Les tests sont du code. Les développeurs écrivent du code. Lorsque les développeurs écrivent du code, ils insèrent environ trois bogues affectant le système par millier de lignes de code. Si nous semons au hasard la base de code de mon client – ce qui inclut les tests – avec de tels bogues, nous constatons que les tests maintiendront le code avec un résultat incorrect plus souvent qu'un véritable bogue provoquera son échec!

Certaines personnes me disent que cela ne s’applique pas à elles, car elles prennent plus de soin à écrire des tests qu’à écrire le code original. Tout d’abord, c’est juste poppycock. (Ceux qui me font vraiment rire sont ceux qui me disent qu'ils sont capables d'oublier les hypothèses qu'ils ont faites lors du codage et d'apporter un nouvel ensemble indépendant à leurs efforts de test. Tous les deux doivent être schizophrènes pour le faire.) Regarder ce que vos développeurs font lors de l'exécution d'une suite de tests: ils sont Faire, ne pas en pensant (comme la plupart du Manifeste Agile, d'ailleurs). Lors de mon premier emploi au Danemark, un projet reposait largement sur XP et les tests unitaires. J'ai fidèlement essayé de créer la version du logiciel sur ma propre machine et, après de nombreuses luttes avec Maven et d'autres outils, j'ai finalement réussi à obtenir une version propre. J'étais anéanti lorsque j'ai constaté que les tests unitaires n'avaient pas réussi. Je suis allé voir mes collègues et ils m'ont dit: "Oh, vous devez invoquer Maven avec ce drapeau qui désactive ces tests. Ce sont des tests qui ne fonctionnent plus en raison de modifications du code et vous devez les désactiver."

Si vous avez 200 tests – ou 2000 ou 10 000 – vous n’allez pas prendre le temps d’enquêter minutieusement et de (re) factoriser chacun d’eux à chaque échec. La pratique la plus courante – que j’ai vue dans une startup où j’avais travaillé jadis en 2005 – consiste simplement à écraser les anciens tests or (résultat attendu ou résultats de calcul à la fin d’un test donné) par les nouveaux résultats. Psychologiquement, la barre verte est la récompense. Les machines rapides d’aujourd’hui donnent l’illusion de pouvoir supplanter la pensée du programmeur; leur vitesse signifie que je ne prends pas le temps de réfléchir. Dans tous les cas, si un client rapporte une erreur et que je suppose où réside le bogue réel, je le modifie de sorte que système le comportement est maintenant juste, je peux facilement être amené à croire que la fonction où j'ai fait le correctif est maintenant bonne. J'écrase donc l'or pour cette fonction. Mais c’est une mauvaise science et s’enracine dans la sorcellerie selon laquelle la corrélation est causalité. Il est également nécessaire de réexécuter toutes les régressions et les tests système.

Deuxièmement, même s’il était vrai que les tests étaient de meilleure qualité que le code en raison d’un processus amélioré ou d’une attention accrue, je conseillerais à l’équipe d’améliorer leur processus afin qu’ils prennent les pilules intelligentes lorsqu’ils écrivent leur code au lieu de l’écrire. leurs tests.

1.8 Vous payez pour des tests de maintenance – et de qualité!

Le fait est que le code fait partie de l'architecture de votre système. Les tests sont des modules. Le fait que celui-ci ne livre pas les tests ne soulage pas l’un des passifs de conception et de maintenance qui viennent avec plus de modules. Une technique souvent confondue avec les tests unitaires, et qui utilise les tests unitaires comme technique, est le développement piloté par les tests. Les gens pensent que cela améliore les métriques de couplage et de cohésion, mais les preuves empiriques montrent autre chose (Janzen et Saledian, l'un des nombreux articles qui réfutent cette notion sur une base empirique, sont les suivants: «Le développement piloté par les tests améliore-t-il vraiment la qualité de la conception de logiciels? Logiciel IEEE 25 (2), mars / avril 2008, p. 77 – 84.) Pour aggraver les choses, vous avez introduit le couplage – changement coordonné – entre chaque module et les tests qui y sont associés.

Quand je regarde la plupart des tests unitaires – en particulier ceux écrits avec JUnit – ils sont des affirmations déguisé. Lorsque j'écris un bon logiciel, je le saupoudre d'assertions qui décrivent les promesses que les appelants de mes fonctions doivent tenir, ainsi que les promesses que cette fonction fait à ses clients. Ces affirmations évoluent dans le même artefact que le reste de mon code. La plupart des environnements contiennent des dispositions permettant de neutraliser administrativement ces assertions lors de l'envoi.

Une approche encore plus professionnelle consiste à laisser les assertions dans le code lors de la livraison, à enregistrer automatiquement un rapport de bogue pour le compte de l'utilisateur final et éventuellement à essayer de redémarrer l'application chaque fois qu'une assertion échoue. À ce même démarrage que j'ai mentionné ci-dessus, j'avais un patron qui a insisté pour que nous ne le fassions pas. Je lui ai fait remarquer qu'un échec d'affirmation signifiait que quelque chose dans le programme était très faux et qu'il était probable que le programme produirait un résultat erroné. Même la plus infime erreur dans le type de logiciel que nous construisions pouvait coûter 5 millions de dollars à un client. Il a dit qu'il était plus important que la société évite l'apparence d'avoir fait quelque chose de mal que de s'arrêter avant de produire un résultat incorrect. J'ai quitté l'entreprise. Peut-être que vous êtes l'un de ses clients aujourd'hui.

☞ Transformez les tests unitaires en assertions. Utilisez-les pour alimenter votre architecture à tolérance de pannes sur les systèmes à haute disponibilité.

Cela résout le problème de la maintenance d'un grand nombre de modules logiciels supplémentaires qui évaluent l'exécution et vérifient le comportement correct; c’est la moitié d’un test unitaire. L'autre moitié est le pilote qui exécute le code: comptez sur vos tests de contrainte, vos tests d'intégration et vos tests système.

Enfin, certains tests unitaires ne font que reproduire des tests système, des tests d'intégration ou d'autres tests. Dans les débuts de l'informatique, lorsque les ordinateurs étaient lents, les tests unitaires donnaient au développeur un retour plus immédiat sur le fait de savoir si une modification avait cassé le code au lieu d'attendre l'exécution des tests du système. Aujourd'hui, avec des ordinateurs moins chers et plus puissants, cet argument est moins convaincant. Chaque fois que je modifie mon application Scrum Knowsy®, je teste au niveau du système. Les développeurs devraient intégrer continuellement et faire des tests de système continuellement plutôt que de se concentrer sur leurs tests unitaires et de remettre à plus tard l'intégration, même d'une heure. Supprimez donc les tests unitaires qui dupliquent ce que les tests système font déjà. Si le niveau de test du système est trop coûteux, créez des tests d'intégration de sous-unités. Rex estime que «le prochain grand pas en avant en matière de test consiste à concevoir des tests unitaires, des tests d'intégration et des tests système de manière à éliminer les écarts et les chevauchements par inadvertance».

☞ Vérifiez votre inventaire de test pour la réplication; vous pouvez financer cela dans le cadre de votre programme Lean. Create system tests with good feature coverage (not code coverage) — remembering that proper response to bad inputs or other unanticipated conditions is part of your feature set.

Last: I once heard an excuse from someone that they needed a unit test because it was impossible to exercise that code unit from any external testing interface. If your testing interfaces are well-designed and can reproduce the kinds of system behaviours you see in the real world, and you find code like this that is unreachable from your system testing methodology, then…. delete the code! Seriously, reasoning about your code in light of system tests can be a great way to find dead code. That’s even more valuable than finding unneeded tests.

1.9 Wrapup

Back to my client from Sogeti. At the outset, I mentioned that he said:

When I was programming on a daily basis, I did make code for testability purposes but I hardly did write any unit tests. However I was renowned for my code quality and my nearly bug free software. I like to investigate WHY did this work for me?

Maybe Richard is one of those rare people who know how to think instead of letting the computer do your thinking for him — be it in system design or low-level design. I tend to find this more in Eastern European countries, where the lack of widely available computing equipment forced people to think. There simply weren’t enough computers to go around. When I first visited Serbia back in 2004, the students at FON (the faculty where one learned computing) could get to a computer to access the Internet une fois par semaine. And the penalty for failure is high: if your code run doesn’t work, you have to wait another week to try again.

I fortunately was raised in a programming culture like this, because my code was on punch cards that you delivered to the operator for queuing up to the machine and then you gathered your output 24 hours later. That forced you to think — or fail. Richard from Sogeti had a similar upbringing: They had a week to prepare their code and just one hour per week to run it. They had to do it right first time. By all means, a learning project should assess the cost impediments and remove another one every iteration, focusing on ever-increasing value. Still, one of my favourite cynical quotes is, “I find that weeks of coding and testing can save me hours of planning.” What worries me most about the fail-fast culture is much less in the échouer que le vite. My boss Neil Haller told me years ago that debugging isn’t what you do sitting in front of your program with a debugger; it’s what you do leaning back in your chair staring at the ceiling, or discussing the bug with the team. However, many supposedly agile nerds put processes and JUnit ahead of individuals and interactions.

The best example was one I heard last year, from a colleague, Nancy Githinji, who used to run a computing company with her husband in Kenya; they both now work at Microsoft. The last time she was back home (last year) she encountered some kids who live out in the jungle and who are writing software. They get to come into town once a month to get access to a computer and try it out. I want to hire those kids!

As an agile guy (and just on principle) it hurts me a little bit to have to admit that Rex is right now and then ☺, but he put it very eloquently: “There’s something really sloppy about this ‘fail fast’ culture in that it encourages throwing a bunch of pasta at the wall without thinking much… in part due to an over-confidence in the level of risk mitigation that unit tests are achieving.” The fail-fast culture can work well with very high discipline, supported by healthy skepticism, but it’s rare to find these attitudes surviving in a dynamic software business. Sometimes failure requires thinking, and that requires more time than would be afforded by failing fast. As my wife Gertrud just reminded me: no one wants a failure to take a long time…

If you hire a professional test manager or testing consultant, they can help you sort out the issues in the bigger testing picture: integration testing, system testing, and the tools and processes suitable to that. It’s important. But don’t forget the Product Owner perspective in Scrum or the business analyst or Program Manager: risk management is squarely in the center of their job, which may be why Jeff Sutherland says that the PO should conceive (and at best conception) the system tests as an input to, or during, Sprint Planning.

As for the Internet: it’s sad, and frankly scary, that there isn’t much out there. There’s a lot of advice, but very little of it is backed either by theory, data, or even a model of why you should believe a given piece of advice. Good testing begs skepticism. Be skeptical of yourself: measure, prove, retry. Be skeptical of moi for heaven’s sake. Write me at jcoplien@gmail.com with your comments and copy Rex at the address at the front of this newsletter.

In summary:

  • Keep regression tests around for up to a year — but most of those will be system-level tests rather than unit tests.
  • Keep unit tests that test key algorithms for which there is a broad, formal, independent oracle of correctness, and for which there is ascribable business value.
  • Except for the preceding case, if X has business value and you can text X with either a system test or a unit test, use a system test — context is everything.
  • Design a test with more care than you design the code.
  • Turn most unit tests into assertions.
  • Throw away tests that haven’t failed in a year.
  • Testing can’t replace good development: a high test failure rate suggests you should shorten development intervals, perhaps radically, and make sure your architecture and design regimens have teeth
  • If you find that individual functions being tested are trivial, double-check the way you incentivize developers’ performance. Rewarding coverage or other meaningless metrics can lead to rapid architecture decay.
  • Be humble about what tests can achieve. Tests don’t improve quality: developers do.