Sep

De nos jours, lorsque l’on souhaite tester son application, on dispose d’une palette d’outils limitée. Certains développeurs vous répondront qu’en utilisant des méthodes de développement telles que le Test Driven Development, on peut avoir un code mieux structuré et une large couverture de tests. Ce qui semble a priori suffisant.

Cependant, il serait pertinent de se poser la question : « est-ce qu’une bonne couverture de tests équivaut à un code bien testé ? ». Il est évident que non. Votre rapport de couverture ne fait que dénombrer et lister les lignes qui ont été exécutées lors des tests. Il ne garantit aucunement leur qualité. Attention, la couverture de code n’est pas inutile : ce n’est qu’une condition nécessaire pour garantir la qualité des tests.

Le cas le plus extrême pour imager ce propos est le test sans assertion. Fort heureusement ce cas de figure ne se produit quasiment jamais. Par contre, les cas de figure que l’on rencontre le plus souvent sont les méthodes de classes partiellement testées.

Dans l’exemple ci-dessous, vous constaterez que la méthode “myMethod” est testée mais la méthode “performMyBusinessLogic” ne l’est jamais directement.

public static String myMethod(boolean b) {
  if(b) {
    performMyBusinessLogic();
    return "OK";
  }
  return "KO";
}

@Test
public void shouldReturnOK() {
  assertEquals("OK", myMethod(true));
}

@Test
public void shouldReturnKO() {
  assertEquals("KO", myMethod(false));
}

Alors comment garantir que les tests que l’on a écrits ou hérités sont de bonne facture ?

Richard J. Lipton, chercheur en informatique du milieu du XXème siècle a dit en 1971 :

If you want to know if a test suite has properly checked some code, introduce a bug

C’est là que PIT entre en jeu. PIT est un plugin maven qui, grâce à une stratégie de « tests mutants », permet de faire évoluer le code exécuté lors des tests. En introduisant des modifications dans vos classes, il permet ainsi d’éprouver vos tests pour voir comment ils réagissent aux changements. Est-ce qu’ils se terminent en échec ou est-ce qu’ils continuent de s’exécuter comme si de rien n’était ?

En effet la logique ici est que si votre code arrive à détecter des bugs mineurs, alors il pourra détecter des bugs plus importants.

Configuration maven

<plugin>
       <groupId>org.pitest</groupId>
       <artifactId>pitest-maven</artifactId>
</plugin>

 

Par défaut PIT va muter tout le code de votre projet (ou tout du moins le code évalué lors des tests), mais vous pouvez le configurer pour limiter les tests pour lequel il sera exécuté et/ou les classes applicatives qu’il pourra muter.

<plugin>
	<groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>LATEST</version>
    <configuration>
		<targetClasses>
			<param>com.your.package.root.want.to.mutate*</param>
		</targetClasses>
		<targetTests>
			<param>com.your.package.root*</param>
		</targetTests>
	<configuration>
</plugin>	

Ensuite pour exécuter le plugin PIT, il faut utiliser la ligne de commande :

mvn org.pitest:pitest-maven:mutationCoverage

A l’issue de son exécution, PIT génère un rapport d’exécution qui détaille pour chaque classe les mutations qui ont été opérées et le résultat de l’exécution.

Les mutateurs

PIT dispose d’un jeu de « mutateurs » qui lui permet de modifier le code exécuté lors des tests. Ci-dessous quelques exemples de mutations :

Opérateur binaire Mutation
< <=
<= <
> >=
>= >
i -i
Valeurs de retour Mutation
return any string “”
Optional Optional.empty()
java.util.List, java.util.Set, java.util.Map Collections.emptyList(), Collections.emptySet

Collections.emptyMap()

Integer, Long, Double, Short, Float 0
any Object null

 

mutation des primitives invariantes
boolean:   true ->false or false -> true
long a -> 0 si a=1 sinon a = a+1
float a -> 0 si a=1.0 ou a=2.0 sinon a = 1.0
double a -> 0.0 si a=1.0 sinon a = 1.0

 

 Il existe, bien sûr, d’autres possibilités de mutations.

Exemples de mutations

example #1:

Soit le scriptlet suivant :

public Object foo(){
   return new Object();
}

Une des mutations possibles sera :

public Object foo(){ 
  new Object(); 
  return null;
}

 

example #2:

Pour le scriptlet suivant :

boolean result = false;
if(number >= 0) {
	result= true;
}
return result;

Les mutations possibles pourront être les suivantes :

boolean result = false;
if(number > 0) {
	result= true;
}
return result;

ou

boolean result = false;
if(false) {
	result= true;
}
return result;

ou encore

boolean result = false;
if(number > 0) {
	result= true;
}
return !result;

 

Il existe plus d’une vingtaine de “mutateurs” qui ont chacun leur rôle et qui mutent le code selon leur stratégie propre. 

PIT permet la sélection ou la désactivation de groupe de mutateurs. Une grande partie d’entre eux sont actifs par défaut.

Pour sélectionner seulement les mutateurs que vous désirez, utilisez la configuration suivante:

<plugin> 
	<groupId>org.pitest</groupId> 
	<artifactId>pitest-maven</artifactId> 
	<version>LATEST</version> 
	<configuration> 
		<targetClasses> 
			<param>com.your.package.root.want.to.mutate*</param> 
		</targetClasses> 
		<targetTests> 
			<param>com.your.package.root*</param> 
		</targetTests> 
		<mutators>
			<mutator>CONSTRUCTOR_CALLS</mutator>
			<mutator>VOID_METHOD_CALLS</mutator>
			<mutator>RETURN_VALS</mutator>
			<mutator>NON_VOID_METHOD_CALLS</mutator>
			<mutator>EMPTY_RETURNS</mutator>
		</mutators>
	<configuration> 
</plugin>

Pour plus de détails: http://pitest.org/quickstart/mutators/

Les tests mutants

Un test mutant est un test exécuté suite à la modification d’une classe chargée dans le contexte d’un test. Chaque modification correspond à un test mutant. Il peut exister plusieurs mutant rien que pour un seul test. 

Si le test échoue après que PIT ait procédé à des modifications, on dit que le mutant est « killed ».

Lorsque PIT modifie une classe dans un contexte d’exécution d’un test et que ce test malgré la modification n’échoue pas, on dit que le mutant est « survived » .

Comme vous pouvez le deviner, c’est l’un des seuls cas où il est bon de tuer ;-). Si vos tests continuent de passer lorsque le code a évolué alors cela peut être un symptôme. Le symptôme de la présence dans votre code de cas qui ne sont pas gérés et qui peuvent être optimisés.

Traitement des mutants

Un bon test unitaire devrait faire échouer tous les mutants.

Lorsque vous avez un test mutant qui a survécu, se présente alors à vous 3 alternatives :

  • Refactorer 
  • Réécrire votre code de manière à pouvoir « killer » les mutants
  • Laisser tel quel

 

Si le mutant effectue exactement le même traitement que le code original malgré la mutation, alors le mutant est dit “équivalent” et il est alors difficile, voire impossible à “killer”. 

Exemple :

Le mutant suivant :

public int compareTo(Object other) {
	
	if(!(ither instanceif MyObject))
		return 0;
	MyObject o = (MyObject) other;
	
	if(prop.getParam() != o.prop.getParam()){
		if(prop.getParam() > o.prop.getParam())
			return 1;
		else
			return -1;
		
	}
}

se trouve clairement dans les bornes d’un code d’origine qui serait :

public int compareTo(Object other) {
	
	if(!(ither instanceif MyObject))
		return 0;
	MyObject o = (MyObject) other;
	
	if(prop.getParam() != o.prop.getParam()){
		if(prop.getParam() > o.prop.getParam())
			return 2; //return 2 in place of 1
		else
			return -1;
		
	}
}

 

  • Ajouter des TU

La génération et l’exécution de mutant par PIT peut dans certains cas révéler des manques quant aux tests déjà présents dans l’application et révéler des cas de tests qui n’ont pas été pensés ou tout simplement pas été écrits. L’implémentation de ces tests pourra « killer » les mutants qui ont survécu à une précédente exécution.

 

Attention : Il est important pour le traitement des tests mutants “survived” de les traiter au cas par cas pour savoir dans quel cas on se trouve et comment les traiter.

 

Conclusion

Bien que la couverture de code reste un outil et une manière de pouvoir juger si notre code est bien testé, il reste néanmoins insuffisant. PIT à travers son mécanisme et des outils mis à disposition permet d’introduire des bugs dans votre code pour tester sa robustesse et voir si celui-ci les a détectés. Facile à installer et à utiliser il est utilisable autant pour les TU que pour les tests d’intégration ou d’acceptance. Littéralement il permet de tester nos tests, et à travers eux le code lui même. Son utilisation peut être une véritable plus value pour les équipes qui mettent l’accent sur la qualité.

Mounir Gzady

Sources:

http://pitest.org/

https://www.youtube.com/watch?v=nf2xpqcZouY&t=688s

Related Posts

Leave A Comment