De l’insuffisance de la couverture de tests

Les besoins en terme de qualité de code amènent de plus en plus de projets à s’intéresser et à coder des Tests Unitaires. Et comme appréhender le logiciel en cours de production est complexe, des indicateurs dits « de couverture de tests » sont utilisés en complément.
Loin de moi l’idée de remettre en cause cet indicateur représentant le pourcentage de code métier exécuté par les tests. Il présente plusieurs intérêts:

  • fournir une valeur quantitative représentant la part de logiciel testé
  • fournir une visualisation (sous forme de rapport html maven ou autre) du code exécuté et surtout du code non exécuté. J’y vois là le double avantage :
  • de mieux maitriser le risque. Le code non testé est connu.
  • de permettre d’améliorer le test lui-même. En effet, du code peut ne pas être testé simplement parce qu’un cas un peu particulier a été omis et visualiser ces portions oubliées du code aide à affiner les tests. D’aucuns diront que si on fait du TDD (« Test Driven Development »), le code non testé est inutile et n’aurait donc pas du être écrit. Je peux les rejoindre dans cette approche extrême. Après, il y a la réalité des projets et du test aujourd’hui…

Reste que cet indicateur de « couverture de test » se contente de « voir » le code exécuté. That’s all et ca n’est peut-être pas suffisant.


Prenons le code métier suivant

public class TestedClass {
	public String myMethod(String str) {
		return str.concat(this.getExtension());	}
	private String getExtension() {
		// in real life, we could imagine this value is retrieved from a
		// properties file or ...
		return ".extension";
	}
}

Prenons le code de test suivant:

public void testMyMethod() {
	TestedClass obj = new TestedClass();
	String param = "test";
	String actualValue = obj.myMethod(param);
	assertNotSame(param, actualValue);
}

Le test semble correct: il exécute une méthode et vérifie un résultat obtenu. La couverture de test sur la méthode myMethod() sera de 100% et permettra d’être satisfait. Toujours est il que dans ce cas, le rôle premier du test – qui est d’assurer la non-régression du code métier – n’est pas rempli. Si le code métier change, le code de test n’échouera pas. Modifions par exemple la méthode qui retourne l’extension:

private String getExtension() {
	// in real life, we could imagine this value is retrieved from a
	// properties file or ...
	return ".otherextension";}

Rejouez le test, il passe, la couverture de test reste à 100% et pourtant une régression a été introduite dans le code métier. Simplement, il manque une assertion assertEquals("test.extension", actualValue);

Dès lors, on se rend compte qu’il manque un indicateur relatif à la pertinence du test: Dans quelle mesure mon test me garantit-il la détection de régressions?

Nos expériences nous donnent deux pistes:

  • Compter le nombre d’assertions (à l’aide d’un grep par exemple). Il semblerait que 5 % d’assertions soit le minimum. « 5% d’assertions », cela signifie que 5 assertions utiles (donc autres que assertTrue(true)…) sont réalisées pour 100 lignes de code métier exécutées. Mais cet indicateur a aussi sa limite et notre exemple simpliste le montre. En effet, 2 lignes de code métier sont éxécutées et une assertion « contextuellement mauvaise » de type assertNotSame est réalisée. Nous sommes donc à 50% d’assertions et pour autant, la regression introduite dans le code n’a pas été détecté par le test…
  • Travailler sur des outils qui introduisent de manière aléatoire des regressions dans le code métier et vérifient si le test échoue ou non. On s’y essaye actuellement et les idées principales reposent:
    • Sur l’utilisation d’AOP pour modifier de manière aléatoire le comportement des méthodes du code métier. A titre d’exemple, on modifie des paramètres ou des résultats de méthodes (inversion du premier et dernier caractère d’un objet de type java.lang.String, multiplication par deux d’un int, lancement d’une exception non-typée en lieu et place du comportement classique de la méthode…)
    • Sur l’utilisation de Maven pour exécuter plusieurs fois les tests « corrompus » ainsi que pour mettre à disposition un rapport html reprenant l’ensemble des modifications effectuées et essayant de donner un indicateur sur la pertinence du harnais de tests

De manière simple, plus la pertinence du test est bonne, plus une modification du code métier sera détectée rapidement. Le test réalisé uniquement avec assertNotSame(param, actualValue); aurait une pertinence quasi null – car aucune modification du code métier ne le ferait échouer – alors que le même test avec l’assertion supplémentaire assertEquals("test.extension", actualValue); deviendrait pertinent.

Ceci étant, je ne vous mentirai pas: il reste du travail :o)

6 commentaires sur “De l’insuffisance de la couverture de tests”

  • Excellente illustration d'un aphorisme : 'Une usine de développement peut prétendre valoriser un actif et un passif, en réalité elle fournira seulement des informations pour discuter de l'actif avec vos utilisateurs et du passif avec vos développeurs.' (le passif est ici l'absence de couverture réelle de tests, qui va induire des coûts futurs de non-régression, l'apparition de bugs, ..).
    On peut sophistiquer les outils de pilotage à l'infini, en réalité il vaut mieux investir sur les mesures de premier ordre (le temps moyen d'un cycle de livraison, le nombre d'incidents/bugs ..) et la motivation des individus à durcir leurs logiciels ...

  • Il faut prendre cet indicateur, non pas dans le sens 'Wouah! Encore 3% et j'ai atteints les 100% de couverture!!' mais 'Argh!! cette classe n'est pas testée!!' ou 'Mince, je n'est que 3% de couverture sur cette classe!!'.
    Au final, la couverture de test est un bon indicateur dans le cas ou il sert pour renseigner le 'moins' et non pas le 'plus'.

  • D'accord avec Nicolas; mon cas d'usage pour cet indicateur, c'est : 'dis-moi vite, quelles sont les classes les moins testées ?' Ca permet d'appréhender rapidement un projet.

    Le chiffre doit être utilisé avec d'autres indicateurs. Seul il ne signifie rien : Olivier a montré qu'un 100% pouvait être factice, et même : 80% est-ce assez bon ? Non, si les 20% non testés contiennent un défaut indétecté qui fera exploser la prod dans 2 ans. Pourquoi pas si les 20% sont par exemple du code ihm simple, testé en intégration..

    L'indicateur de couverture est donc une mesure 'négative' (pas au sens psychologique, je précise) : il peut montrer du code insuffisamment testé, il ne peut pas montrer que le code est suffisamment testé. Intéressant : l'outil de sabotage (changer une ligne de code, rejouter les tests: si les tests passent, le code est pas suffisamment couvert) délivre également lui-même une autre mesure 'négative' : il permet de montrer que certains sabotages n'ont pas cassé les tests, mais pas que les tests détecteraient tout sabotage.

  • Ce qui m'amène à penser qu'il pourrait être également interressant d'associer la politique de test à une (mini) étude de risque (risk = (Damage + Reproductibility + ...). Quels sont mes flux métiers qui ne doivent jamais être en erreur?

    Cela pourrait aider à choisir et à prioriser l'effort de test.

  • Les tests unitaires sont des tests réalisés lors du développement et généralement par les développeurs pour s'assurer que fonctions et classes font bien ce qui est attendu (par eux).
    Les développements sont alors une 'boîte blanche'.
    La couverture du plan de test peut se ramener à celle du graphe représenté par les différentes classes, cas, etc...

    Les tests fonctionnels sont construits à partir des spécifications 'fonctionnelles' et en général 'avant' ou en parallèle avec les développements.
    Les développements sont alors une 'boîte noire' et le plan de tests se pré-occupe du comportement des interfaces et non de l'implémentation.
    La couverture fonctionnelle du plan de tests est plus difficile à construire et pourra être définie avec des variations de scenarii bien établis.
    On ne parlera de régression fonctionnelle qu'après le passage infructueux de tests fonctionnels suite à une mise à jour du logiciel (testé).
    Cette mise à jour implique côté développements, la mise à jour ou l'écriture d'un certain nombre de tests unitaires : normal, ils collent à l'implémentation.

  • Cet article illustre le fait que le moyen de vérification a une aussi grande importance que l'objet de sa vérification (le produit).

    Peut-être qu'avec des moyens de preuves mathématiques on pourra arriver à prouver le bon fonctionnement des programmes (c'est un thème de recherche du laboratoire commun Microsoft Reseach-INRIA).

    En attendant, cet article démontre que effectivement on n'est pas encore arrivé au stade d'industrialisation d'outils de preuve de bon fonctionnement des programmes étant donné qu'il n'y a pas de moyen systématique de preuve.

    1. Laisser un commentaire

      Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha