Les tests unitaires conduisent-ils à une généralisation prématurée (en particulier dans le contexte du C ++)?

20

Notes préliminaires

Je n'entrerai pas dans la distinction des différents types de tests, il y a déjà quelques questions sur ces sites à ce sujet.

Je prends ce qui est là et qui dit: test unitaire dans le sens de "tester la plus petite unité isolable d'une application" dont dérive cette question

Le problème de l'isolement

Quelle est la plus petite unité isolable d'un programme. Eh bien, comme je le vois, cela dépend (fortement?) De la langue dans laquelle vous codez.

Micheal Feathers parle du concept d'une couture : [WEwLC, p31]

Une couture est un endroit où vous pouvez modifier le comportement de votre programme sans modifier à cet endroit.

Et sans entrer dans les détails, je comprends qu'une couture - dans le contexte des tests unitaires - est un endroit dans un programme où votre "test" peut s'interfacer avec votre "unité".

Exemples

Le test unitaire - en particulier en C ++ - nécessite du code sous test d'ajouter plus de coutures qui seraient strictement requises pour un problème donné.

Exemple:

  • Ajout d'une interface virtuelle où une implémentation non virtuelle aurait été suffisante
  • Fractionnement - généralisation (?) - une classe (plus petite) plus "juste" pour faciliter l'ajout d'un test.
  • Fractionnement d'un projet exécutable unique en bibliothèques apparemment "indépendantes", "juste" pour faciliter leur compilation indépendante pour les tests.

La question

J'essaierai quelques versions qui, espérons-le, poseront la même question:

  • Est-ce la façon dont les tests unitaires exigent que l'on structure le code d'une application "uniquement" bénéfique pour les tests unitaires ou est-ce réellement bénéfique pour la structure des applications.
  • La généralisation du code qui est nécessaire pour le rendre testable unitaire est-elle utile pour autre chose que les tests unitaires?
  • L'ajout de tests unitaires force-t-il à généraliser inutilement?
  • Les tests unitaires de forme imposés au code sont-ils "toujours" également une bonne forme pour le code en général, vu du domaine problématique?

Je me souviens d'une règle empirique qui disait de ne pas généraliser jusqu'à ce que vous ayez besoin de / jusqu'à ce qu'il y ait une deuxième place qui utilise le code. Avec les tests unitaires, il y a toujours une deuxième place qui utilise le code - à savoir le test unitaire. Cette raison est-elle donc suffisante pour généraliser?

Martin Ba
la source
8
Un mème commun est que tout modèle peut être utilisé de manière excessive pour devenir un anti-modèle. La même chose est vraie avec TDD. On peut ajouter des interfaces testables au-delà du point de rendements décroissants, où le code testé est inférieur aux interfaces de test généralisées ajoutées, ainsi que dans la zone de coût-bénéfice trop faible. Un jeu occasionnel avec des interfaces supplémentaires pour des tests comme un OS de mission dans l'espace lointain pourrait complètement manquer sa fenêtre de marché. Assurez-vous que le test ajouté est avant ces points d'inflexion.
hotpaw2
@ hotpaw2 Blasphème! :)
maple_shaft

Réponses:

23

Les tests unitaires - en particulier en C ++ - nécessitent du code testé d'ajouter plus de coutures qui seraient strictement nécessaires pour un problème donné.

Seulement si vous n'envisagez pas de tester une partie intégrante de la résolution de problèmes. Pour tout problème non trivial, il devrait l'être, non seulement dans le monde du logiciel.

Dans le monde du matériel, cela a été appris depuis longtemps - à la dure. Les fabricants de divers équipements ont appris au fil des siècles d'innombrables ponts qui tombent, des voitures qui explosent, des processeurs qui fument, etc., etc. ce que nous apprenons maintenant dans le monde du logiciel. Tous intègrent des «coutures supplémentaires» dans leurs produits afin de les rendre testables. De nos jours, la plupart des voitures neuves disposent de ports de diagnostic permettant aux réparateurs d'obtenir des données sur ce qui se passe à l'intérieur du moteur. Une partie importante des transistors de chaque CPU sert à des fins de diagnostic. Dans le monde du matériel, chaque élément «supplémentaire» coûte, et lorsqu'un produit est fabriqué par millions, ces coûts représentent certainement de grosses sommes d'argent. Pourtant, les fabricants sont prêts à dépenser tout cet argent pour la testabilité.

De retour au monde du logiciel, C ++ est en effet plus difficile à tester unitaire que les langages ultérieurs comportant un chargement de classe dynamique, une réflexion, etc. Pourtant, la plupart des problèmes peuvent être au moins atténués. Dans le projet C ++ où j'ai utilisé des tests unitaires jusqu'à présent, nous n'avons pas exécuté les tests aussi souvent que nous le ferions par exemple dans un projet Java - mais ils faisaient toujours partie de notre build CI, et nous les avons trouvés utiles.

Est-ce que la façon dont les tests unitaires nécessitent de structurer le code d'une application est "uniquement" bénéfique pour les tests unitaires ou est-ce réellement bénéfique pour la structure des applications?

D'après mon expérience, une conception testable est globalement bénéfique, pas "seulement" pour les tests unitaires eux-mêmes. Ces avantages se présentent à différents niveaux:

  • Rendre votre conception testable vous oblige à découper votre application en petites parties plus ou moins indépendantes qui ne peuvent s'influencer les unes que les autres de manière limitée et bien définie - cela est très important pour la stabilité et la maintenabilité à long terme de votre programme. Sans cela, le code a tendance à se détériorer en code spaghetti où toute modification apportée à n'importe quelle partie de la base de code peut provoquer des effets inattendus dans des parties distinctes et apparemment indépendantes du programme. Inutile de dire que c'est le cauchemar de tout programmeur.
  • L'écriture des tests eux-mêmes à la manière de TDD exerce en fait vos API, vos classes et vos méthodes et sert de test très efficace pour détecter si votre conception a du sens - si l'écriture de tests et l'interface vous semble gênante ou difficile, vous obtenez de précieuses informations précoces lorsqu'elle est encore facile de façonner l'API. En d'autres termes, cela vous empêche de publier vos API prématurément.
  • Le modèle de développement imposé par TDD vous aide à vous concentrer sur la ou les tâches concrètes à faire et vous maintient sur la cible, minimisant les chances que vous vous éloigniez de résoudre d'autres problèmes que celui que vous êtes censé ajouter, ajoutant des fonctionnalités supplémentaires inutiles et de la complexité , etc.
  • La rétroaction rapide des tests unitaires vous permet d'être audacieux dans la refactorisation du code, vous permettant d'adapter et d'évoluer constamment la conception au cours de la durée de vie du code, empêchant ainsi efficacement l'entropie du code.

Je me souviens d'une règle empirique qui disait de ne pas généraliser jusqu'à ce que vous ayez besoin de / jusqu'à ce qu'il y ait une deuxième place qui utilise le code. Avec les tests unitaires, il y a toujours une deuxième place qui utilise le code - à savoir le test unitaire. Cette raison est-elle donc suffisante pour généraliser?

Si vous pouvez prouver que votre logiciel fait exactement ce qu'il est censé faire - et le prouver d'une manière suffisamment rapide, reproductible, bon marché et déterministe pour satisfaire vos clients - sans la généralisation «supplémentaire» ou les coutures forcées par les tests unitaires, allez-y (et dites-nous comment vous le faites, car je suis sûr que beaucoup de gens sur ce forum seraient aussi intéressés que moi :-)

Btw Je suppose que par "généralisation", vous voulez dire des choses comme l'introduction d'une interface (classe abstraite) et le polymorphisme au lieu d'une seule classe concrète - sinon, veuillez clarifier.

Péter Török
la source
Monsieur, je vous salue.
GordonM
Une note brève mais pédante: le «port de diagnostic» est surtout là parce que les gouvernements les ont mandatés dans le cadre d'un programme de contrôle des émissions. Par conséquent, il a de graves limites; il y a beaucoup de choses qui pourraient potentiellement être diagnostiquées avec ce port qui ne le sont pas (c'est-à-dire tout ce qui n'a rien à voir avec le contrôle des émissions).
Robert Harvey
4

Je vais vous lancer La Voie du Testivus , mais pour résumer:

Si vous passez beaucoup de temps et d'énergie à rendre votre code plus compliqué pour tester une seule partie du système, il se peut que votre structure soit incorrecte ou que votre approche de test soit incorrecte.

Le guide le plus simple est le suivant: ce que vous testez est l'interface publique de votre code de la manière dont il est destiné à être utilisé par d'autres parties du système.

Si vos tests deviennent longs et compliqués, c'est une indication que l'utilisation de l'interface publique va être difficile.

Si vous devez utiliser l'héritage pour permettre à votre classe d'être utilisée par autre chose que l'instance unique pour laquelle elle sera actuellement utilisée, il y a de fortes chances que votre classe soit trop fortement liée à son environnement d'utilisation. Pouvez-vous donner un exemple d'une situation où cela est vrai?

Cependant, méfiez-vous du dogme des tests unitaires. Écrivez le test qui vous permet de détecter le problème qui vous fera crier dessus .

deworde
la source
J'allais ajouter la même chose: faire une api, tester l'api, de l'extérieur.
Christopher Mahan
2

TDD et tests unitaires, est bon pour le programme dans son ensemble, et pas seulement pour les tests unitaires. La raison en est que c'est bon pour le cerveau.

Il s'agit d'une présentation d'un framework ActionScript spécifique nommé RobotLegs. Cependant, si vous parcourez les 10 premières diapositives, cela commence à atteindre les bonnes parties du cerveau.

Les tests TDD et unitaires vous obligent à vous comporter de manière à ce que le cerveau traite et mémorise mieux les informations. Donc, alors que votre tâche exacte devant vous est simplement de faire un meilleur test unitaire, ou de rendre le code plus testable unitaire ... ce qu'il fait réellement est de rendre votre code plus lisible, et donc de rendre votre code plus maintenable. Cela vous permet de coder plus rapidement en habbits et vous permet de comprendre votre code plus rapidement lorsque vous devez ajouter / supprimer des fonctionnalités, corriger des bogues ou, en général, ouvrir le fichier source.

Bob
la source
1

tester la plus petite unité isolable d'une application

c'est vrai, mais si vous allez trop loin, cela ne vous donne pas grand-chose, et cela coûte cher, et je pense que c'est cet aspect qui fait la promotion de l'utilisation du terme BDD pour être ce que le TDD aurait dû être tout. le long - la plus petite unité isolable est ce que vous voulez qu'elle soit.

Par exemple, j'ai une fois débogué une classe réseau qui avait (entre autres bits) 2 méthodes: 1 pour définir l'adresse IP, une autre pour définir le numéro de port. Naturellement, ces méthodes étaient très simples et passeraient facilement le test le plus trivial, mais si vous définissez le numéro de port, puis définissez l'adresse IP, cela ne fonctionnerait pas - l'IP setter remplaçait le numéro de port par défaut. Donc, vous avez dû tester la classe dans son ensemble pour vous assurer d'un comportement correct, quelque chose que je pense que le concept de TDD manque mais BDD vous donne. Vous n'avez pas vraiment besoin de tester chaque petite méthode, lorsque vous pouvez tester la zone la plus sensible et la plus petite de l'application globale - dans ce cas, la classe de mise en réseau.

En fin de compte, il n'y a pas de solution miracle aux tests, vous devez prendre des décisions judicieuses quant à la quantité et à la granularité auxquelles appliquer vos ressources de test limitées. L'approche basée sur les outils qui génère automatiquement des talons pour vous ne le fait pas, c'est une approche à force contondante.

Donc, étant donné cela, vous n'avez pas besoin de structurer votre code d'une certaine manière pour atteindre TDD, mais le niveau de test que vous réalisez dépendra de la structure de votre code - si vous avez une interface graphique monolithique qui a toute sa logique étroitement liée à la structure de l'interface graphique, alors vous aurez du mal à isoler ces éléments, mais vous pouvez toujours écrire un test unitaire où `` unité '' se réfère à l'interface graphique et tout le travail de base de données est moqué. C'est un exemple extrême, mais il montre que vous pouvez toujours faire des tests automatisés dessus.

Un effet secondaire de la structuration de votre code pour faciliter le test de petites unités vous aide à mieux définir l'application et vous permet de remplacer plus facilement les pièces. Cela aide également lors du codage, car il est moins probable que 2 développeurs travaillent sur le même composant à un moment donné - contrairement à une application monolithique qui a des dépendances entremêlées qui interrompent le travail de tous les autres.

gbjbaanb
la source
0

Vous avez atteint une bonne prise de conscience des compromis dans la conception de langage. Certaines des décisions de conception de base en C ++ (le mécanisme de fonction virtuelle mélangé au mécanisme d'appel de fonction statique) rendent TDD difficile. La langue ne prend pas vraiment en charge ce dont vous avez besoin pour vous faciliter la tâche. Il est facile d'écrire C ++ qui est presque impossible à tester unitaire.

Nous avons eu plus de chance de faire notre code TDD C ++ à partir d'un état d'esprit quasi fonctionnel, d'écrire des fonctions et non des procédures (une fonction qui ne prend aucun argument et renvoie vide), et d'utiliser la composition dans la mesure du possible. Comme il est difficile de remplacer ces classes membres, nous nous concentrons sur le test de ces classes pour construire une base de confiance, puis savons que l'unité de base fonctionne lorsque nous l'ajoutons à autre chose.

La clé est l'approche quasi-fonctionnelle. Pensez-y, si tout votre code C ++ était des fonctions libres qui n'accédaient à aucun global, ce serait un clin d'œil au test unitaire :)

anon
la source