De l'insuffisance de la couverture de tests

le 23/01/2007 par Olivier Mallassi
Tags: Software Engineering

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)