Tests par mutations : testez vos tests [1/2]

Les tests unitaires et d'intégration permettent de vérifier le fonctionnement d’une application et d’empêcher les régressions. Mais comment faire pour vérifier la qualité des tests ?
Une couverture du code par les tests à 100% ne signifie pas que le code est testé à 100%. Cette valeur indique seulement le pourcentage de code exécuté lors du passage des tests.

Une solution à cela : les tests par mutations (mutation testing). Ceux-ci permettent de vérifier la cohérence et la pertinence des tests unitaires.

Je vous présenterai, dans cet article le principe des tests par mutations. Dans un second, je vous montrerai quelques exemples d'application avec un outil permettant de contrôler la qualité de tests Java (JUnit, Test NG).

Vos 100% de couverture de code ? Une illusion !

Les tests peuvent être utilisés pour mettre en évidence la présence de bugs dans un programme mais jamais leur absence. Ainsi, les outils de couverture de code (Clover, Cobertura) ont un gros défaut. Ils indiquent seulement si des blocs de code ont été exécutés lors des tests unitaires. C’est pourquoi, même un test qui ne teste rien (pas d’assertion) peut couvrir 100% de la méthode testée.

Exemple de test qui ne teste rien, malgré une couverture de 100% :
test_inutile.png

Ceci est un exemple extrême, bien que cela existe. La plupart du temps, nous avons des tests qui vérifient partiellement notre code (pas de test aux limites par exemple).

Les tests par mutations

Les tests par mutations ont été inventés dans les années 1970 dans le but de localiser les faiblesses dans les suites de tests. La théorie était que si une modification était introduite dans le code source sans que le comportement du programme soit affecté, alors cela indiquait soit que le code modifié n’était jamais exécuté (code mort), soit que les tests était incapables de détecter la modification. Pour ce faire, un nombre considérable de mutations devaient être introduites dans les programmes, ce qui entraînait la compilation et l’exécution d’un nombre extrêmement grand de copies de ceux-ci. Du fait de ce coût, l'utilisation des tests par mutations n’était pas répandue, mais l’utilisation croissante de langages orientés objets et de frameworks de tests a mené a la création d’outils permettant de tester unitairement une applications.

L’idée, est donc,de modifier le code source de notre application (= créer des bugs) et de vérifier que nos tests détectent les modifications (=nos tests doivent échouer). Si nos tests passent toujours, c’est qu’ils ne sont pas pertinents.

En pratique

On exécute une première fois les tests pour vérifier qu’ils passent bien.

Ensuite, les tests par mutations se font généralement en 4 phases :

  • La génération de mutants;
  • La sélection des tests qui rechercheront les mutants ;
  • L’insertion des mutants ;
  • La recherche des mutants.

La phase de génération de mutants consiste à parcourir toutes les instructions du code métier et pour chacune déterminer si des mutations sont applicables. Si oui, chaque mutation donnera naissance à un nouveau mutant. Voici quelques exemples de mutations applicables :
- Modification des bornes des opérateurs de comparaison : > devient >= ;
- Négation des opérateurs de comparaison : == devient != ;
- Modification des opérateurs mathématiques : + devient - ;
- suppression d’instructions ;
etc.

La phase de sélection des tests peut être réalisée de plusieurs façons :
- Manuelle : l’utilisateur sélectionne manuellement les tests.
- Par convention : la sélection des tests est automatique et se fait par convention de nommage, annotations…
- Par couverture : la sélection est automatique. Seuls les tests qui exécutent les lignes de codes contenant les mutants sont sélectionnés.

La phase de recherche des mutants se fait de la manière suivante. On exécute les tests (ciblés) pour chaque mutant. Si un des tests échoue, on dit que le mutant est tué. Dans le cas contraire, on dit qu’il a survécu.

A la fin, on calcule un score par classe : nombre de mutants tués divisé par le nombre de mutants créés. Le but étant d’avoir un score de 100%.

Avantages / Inconvénients des tests par mutations

Avantages :

  • permet de trouver des cas non testés
  • ne requiert pas de développement spécifique en plus des tests unitaires
  • le principe est simple

Inconvénients :

  • Cela peut être très long à exécuter (volume de mutations possibles est exponentielle)
  • L’analyse des résultats peut être fastidieuse (idem)
  • Il arrive qu’il y ait des mutants impossible à tuer (mutants équivalents). Cela se produit dans les cas où une mutation remplace une instruction par une autre sémantiquement équivalente.

> Lire la suite (Exemple d'application)

Références

Ici, et .

Laisser un commentaire

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

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.