Comment améliorer considérablement la couverture du code?

21

Je suis chargé d'obtenir une application héritée sous test unitaire. Tout d'abord quelques informations sur l'application: il s'agit d'une base de code Java RCP 600k LOC avec ces problèmes majeurs

  • duplication massive de code
  • pas d'encapsulation, la plupart des données privées sont accessibles de l'extérieur, certaines des données commerciales ont également fait des singletons donc elles ne sont pas seulement modifiables de l'extérieur mais aussi de partout.
  • pas d'abstractions (par exemple pas de modèle commercial, les données commerciales sont stockées dans Object [] et double [] []), donc pas d'OO.

Il existe une bonne suite de tests de régression et une équipe d'AQ efficace teste et trouve des bogues. Je connais les techniques pour le tester dans des livres classiques, par exemple Michael Feathers, mais c'est trop lent. Comme il existe un système de test de régression fonctionnel, je n'ai pas peur de refactoriser agressivement le système pour permettre l'écriture de tests unitaires.

Comment dois-je commencer à attaquer le problème pour obtenir rapidement une couverture , donc je peux montrer les progrès à la direction (et en fait commencer à gagner du filet de sécurité des tests JUnit)? Je ne veux pas utiliser d'outils pour générer des suites de tests de régression, par exemple AgitarOne, car ces tests ne testent pas si quelque chose est correct.

Peter Kofler
la source
Pourquoi ne pas créer automatiquement les tests de régression et les vérifier individuellement? Je dois être plus rapide que de les écrire tous à la main.
Robert Harvey
Cela peut sembler un peu drôle d'appeler tout ce qui est écrit en héritage Java, mais d'accord, c'est certainement hérité. Vous mentionnez que vous n'avez pas peur de refactoriser le système pour permettre l'écriture de tests unitaires, mais ne devriez-vous pas écrire les tests unitaires sur le système tel quel, avant toute refactorisation? Ensuite, votre refactoring peut être exécuté à travers les mêmes tests unitaires pour vous assurer que rien n'est cassé?
dodgy_coder
1
@dodgy_coder D'habitude d'accord, mais j'espère que le QA traditionnel qui fonctionne efficacement me ferait gagner du temps.
Peter Kofler
1
@dodgy_coder Michael C. Feathers, auteur de Travailler efficacement avec le code hérité définit le code hérité comme «code sans tests». Il sert de définition utile.
StuperUser

Réponses:

10

Je crois qu'il y a deux axes principaux le long desquels le code peut être placé lorsqu'il s'agit d'introduire des tests unitaires: A) dans quelle mesure le code est-il testable? et B) dans quelle mesure est-il stable (c.-à-d. à quel point a-t-il besoin d'urgence de tests)? En ne regardant que les extrêmes, cela donne 4 catégories:

  1. Code facile à tester et fragile
  2. Code facile à tester et stable
  3. Code difficile à tester et fragile
  4. Code difficile à tester et stable

La catégorie 1 est le point de départ évident, où vous pouvez obtenir beaucoup d'avantages avec relativement peu de travail. La catégorie 2 vous permet d'améliorer rapidement votre statistique de couverture (bonne pour le moral) et d'acquérir plus d'expérience avec la base de code, tandis que la catégorie 3 est plus (souvent frustrante), mais donne également plus d'avantages. Ce que vous devez faire en premier dépend de l'importance des statistiques de moral et de couverture pour vous. La catégorie 4 ne vaut probablement pas la peine d'être dérangée.

Michael Borgwardt
la source
Excellent. J'ai une idée comment déterminer s'il est facile de vérifier par analyse statique, par exemple le nombre de dépendances / l'explorateur de testabilité. Mais comment pourrais-je déterminer si le code est fragile? Je ne peux pas faire correspondre les défauts à des unités spécifiques (par exemple les classes), et si c'est le numéro 3 bien sûr (les classes divines / singletons). Alors peut-être nombre de checkins (les hotspots)?
Peter Kofler
1
@Peter Kofler: les hotspots de commit sont une bonne idée, mais la source la plus précieuse de ce type de connaissances serait les développeurs qui ont travaillé avec le code.
Michael Borgwardt
1
@Peter - comme Michael l'a dit, les développeurs qui ont travaillé avec le code. Quiconque a travaillé avec une grande base de code pendant un bon bout de temps saura quelles parties en sentent. Ou, si le tout sent, quelles parties sentent vraiment mauvais .
Carson63000
15

J'ai beaucoup d'expérience en travaillant sur des systèmes hérités (pas Java cependant), beaucoup plus gros que cela. Je déteste être porteur de mauvaises nouvelles, votre problème est la taille de votre problème. Je soupçonne que vous l'avez sous-estimé.

L'ajout de tests de régression au code hérité est un processus lent et coûteux. De nombreuses exigences ne sont pas bien documentées - un correctif de bogue ici, un correctif là-bas, et avant de le savoir, le logiciel définit son propre comportement. Ne pas avoir de tests signifie que l'implémentation est tout ce qu'il y a à faire, pas de tests pour "contester" les exigences implicites implémentées dans le code.

Si vous essayez d'obtenir une couverture rapidement, il est probable que vous précipitiez le travail, le fassiez à moitié et échouiez. Les tests donneront une couverture partielle des éléments évidents et une couverture médiocre à nulle des problèmes réels. Vous convaincrez les gestionnaires que vous essayez de vendre à ce test unitaire ne vaut pas la peine, que c'est juste une autre solution miracle qui ne fonctionne pas.

À mon humble avis, la meilleure approche est de cibler vos tests. Utilisez des métriques, des intuitions et des rapports de journal des défauts pour identifier le 1% ou 10% de code qui génère le plus de problèmes. Frappez durement ces modules et ignorez le reste. N'essayez pas d'en faire trop, moins c'est plus.

Un objectif réaliste est "Depuis que nous avons implémenté l'UT, l'insertion de défauts dans les modules testés a chuté à x% de ceux qui ne sont pas sous UT" (idéalement, x est un nombre <100).

mattnz
la source
+1, vous ne pouvez pas tester un élément efficacement sans un standard plus solide que le code.
dan_waterworth
Je sais et je suis d'accord. La différence est que nous avons mis en place des tests, des tests de régression traditionnels en travaillant l'AQ, il existe donc une sorte de filet de sécurité. Deuxièmement, je suis profondément en faveur des tests unitaires, donc ce ne sera certainement pas une autre chose qui n'a pas fonctionné. Un bon point sur ce qu'il faut viser en premier. Merci.
Peter Kofler
1
et n'oubliez pas que le simple fait de viser la "couverture" n'améliorera pas la qualité, car vous allez vous retrouver coincé dans un bourbier de tests défectueux et triviaux (et de tests pour du code trivial qui n'a pas besoin de tests explicites, mais sont ajoutés juste pour augmenter la couverture). Vous allez finir par créer des tests pour plaire à l'outil de couverture, non pas parce qu'ils sont utiles, et éventuellement changer le code lui-même pour augmenter la couverture des tests sans écrire de tests (comme couper les commentaires et les définitions de variables, ce que certains outils de couverture appellera le code non couvert).
jwenting
2

Je me souviens de ce dicton de ne pas s'inquiéter de la porte de la grange lorsque le cheval a déjà boulonné.

La réalité est qu'il n'y a vraiment pas de moyen rentable d'obtenir une bonne couverture de test pour un système hérité, certainement pas dans un court laps de temps. Comme MattNz l'a mentionné, ce sera un processus très long, et finalement extrêmement coûteux. Mon instinct me dit que si vous essayez de faire quelque chose pour impressionner la direction, vous créerez probablement un nouveau cauchemar de maintenance parce que vous essayez de montrer trop rapidement, sans bien comprendre les exigences que vous essayez de tester.

De façon réaliste, une fois que vous avez déjà écrit le code, il est presque trop tard pour écrire les tests efficacement sans risquer de manquer quelque chose de vital. D'un autre côté, on pourrait dire que certains tests valent mieux qu'aucun test, mais si c'est le cas, les tests eux-mêmes doivent montrer qu'ils ajoutent de la valeur au système dans son ensemble.

Ma suggestion serait d'examiner les domaines clés où vous sentez que quelque chose est "cassé". J'entends par là qu'il pourrait s'agir d'un code terriblement inefficace, ou que vous pouvez démontrer qu'il était auparavant coûteux à entretenir. Documentez les problèmes, puis utilisez-les comme point de départ pour introduire un niveau de test qui vous aide à améliorer le système, sans vous lancer dans un effort massif de réingénierie. L'idée ici est d'éviter de rattraper les tests, et d'introduire des tests pour vous aider à résoudre des problèmes spécifiques. Après un certain temps, voyez si vous pouvez mesurer et distinguer entre le coût précédent de la maintenance de cette section de code et les efforts actuels avec les correctifs que vous avez appliqués avec leurs tests de prise en charge.

La chose à retenir est que la direction s'intéresse davantage aux coûts / avantages et à la façon dont cela affecte directement leurs clients et, finalement, leur budget. Ils ne sont jamais intéressés à faire quelque chose simplement parce que c'est la meilleure chose à faire, à moins que vous ne puissiez prouver que cela leur procurera un avantage qui les intéresse. Si vous êtes en mesure de montrer que vous améliorez le système et obtenez une bonne couverture de test pour le travail que vous effectuez actuellement, la direction est plus susceptible de voir cela comme une application efficace de vos efforts. Cela pourrait éventuellement vous permettre de plaider en faveur d'une extension de vos efforts à d'autres domaines clés, sans exiger soit un gel complet du développement du produit, ou pire encore le quasi impossible de plaider pour une réécriture!

S.Robins
la source
1

Une façon d'améliorer la couverture est d'écrire plus de tests.

Une autre façon est de réduire la redondance dans votre code, de telle sorte que les tests existants couvrent en fait le code redondant non couvert actuellement.

Imaginez que vous avez 3 blocs de code, a, b et b ', où b' est un doublon (copie exacte ou presque manquante) de B, et que vous avez une couverture sur a et b mais pas b 'avec le test T.

Si refactorisez la base de code pour éliminer b 'en extrayant les points communs de b et b' comme B, la base de code ressemble maintenant à a, b0, B, b'0, avec b0 contenant le code non partagé avec b'0 et vice- inversement, et b0 et b'0 étant beaucoup plus petits que B, et invoquant / utilisant B.

Maintenant, la fonctionnalité du programme n'a pas changé, et ni l'un ni l'autre n'a testé T, nous pouvons donc réexécuter T et nous attendre à ce qu'il passe. Le code maintenant couvert est a, b0 et B, mais pas b'0. La base de code est devenue plus petite (b'0 est plus petit que b '!) Et nous couvrons toujours ce que nous avons couvert à l'origine. Notre taux de couverture a augmenté.

Pour ce faire, vous devez trouver b, b 'et idéalement B pour permettre votre refactoring. Notre outil CloneDR peut le faire pour de nombreuses langues, notamment Java. Vous dites que votre base de code contient beaucoup de code dupliqué; cela pourrait être un bon moyen de l'aborder à votre avantage.

Curieusement, l'acte de trouver b et b 'augmente souvent votre vocabulaire sur les idées abstraites que le programme met en œuvre. Bien que l'outil n'ait aucune idée de ce que font b et b ', le fait même de les isoler du code, permettant une concentration simple sur le contenu de b et b', donne souvent aux programmeurs une très bonne idée de l'abstraction B_abstract que le code cloné implémente . Donc, cela améliore également votre compréhension du code. Assurez-vous de donner un bon nom à B lorsque vous vous en abstenez, et vous obtiendrez une meilleure couverture de test et un programme plus maintenable.

Ira Baxter
la source