Avez-vous des tests pour toutes les occasions? Ou peut-être que le référentiel de votre projet contient même l'aide «À propos de 100% de couverture de test»? Mais est-ce que tout est si simple et réalisable dans la vraie vie?
Avec les tests unitaires, tout est plus ou moins clair: ils doivent être écrits. Parfois, ils ne fonctionnent pas comme prévu: il y a des faux positifs ou des tests avec des erreurs qui renverront oui et non sans aucune modification de code. Les petits bogues que vous pouvez trouver dans les tests unitaires sont précieux, mais souvent le développeur les corrigera avant de s'engager. Mais nous sommes vraiment inquiets de ces erreurs qui échappent souvent à la vue. Et pire que tout, ils décident souvent de se faire un nom au moment même où le produit tombe entre les mains de l'utilisateur.
C'est un test de mutationvous permet de gérer ces bugs insidieux. Il modifie le code source d'une manière prédéterminée (en introduisant des bogues spéciaux - les soi-disant «mutants») et vérifie si ces mutants survivent dans d'autres tests. Tout mutant qui a survécu au test unitaire conduit à la conclusion que les tests standard n'ont pas trouvé le morceau de code modifié correspondant contenant l'erreur.
En Python, mutmut est le principal outil de test de mutation .
Imaginons que nous devions écrire un code qui calcule l'angle entre les aiguilles des heures et des minutes dans une horloge analogique:
def hours_hand(hour, minutes):
base = (hour % 12 ) * (360 // 12)
correction = int((minutes / 60) * (360 // 12))
return base + correction
def minutes_hand(hour, minutes):
return minutes * (360 // 60)
def between(hour, minutes):
return abs(hours_hand(hour, minutes) - minutes_hand(hour, minutes))
Écrivons un test unitaire de base:
import angle
def test_twelve():
assert angle.between(12, 00) == 0
Il n'y a pas ifs dans le code . Vérifions dans quelle mesure un tel test unitaire couvre toutes les situations possibles:
$ coverage run `which pytest`
============================= test session starts ==============================
platform linux -- Python 3.8.3, pytest-5.4.3, py-1.8.2, pluggy-0.13.1
rootdir: /home/moshez/src/mut-mut-test
collected 1 item
tests/test_angle.py . [100%]
============================== 1 passed in 0.01s ===============================
Excellent! Comme une couverture à 100%. Mais que se passe-t-il lorsque nous faisons des tests de mutation?
Oh non! Sur les 21, pas moins de 16 mutants ont survécu. Comment?
Pour chaque test de mutation, une partie du code source doit être modifiée pour simuler une erreur potentielle. Un exemple d'une telle modification consiste à changer l'opérateur de comparaison ">" en "> =". S'il n'y a pas de test unitaire pour cette condition aux limites, ce bogue mutant survivra: c'est un bogue potentiel qu'aucun des tests habituels ne détectera.
D'accord. Tout est clair. Nous devons écrire de meilleurs tests unitaires. Ensuite, à l'aide de la commande results, voyons quelles modifications spécifiques ont été apportées:
$ mutmut results
<snip>
Survived :( (16)
---- angle.py (16) ----
4-7, 9-14, 16-21
$ mutmut apply 4
$ git diff
diff --git a/angle.py b/angle.py
index b5dca41..3939353 100644
--- a/angle.py
+++ b/angle.py
@@ -1,6 +1,6 @@
def hours_hand(hour, minutes):
hour = hour % 12
- base = hour * (360 // 12)
+ base = hour / (360 // 12)
correction = int((minutes / 60) * (360 // 12))
return base + correction
Voici un exemple typique du fonctionnement de mumut: il analyse le code source et remplace certains opérateurs par d'autres: par exemple, addition par soustraction ou, comme dans ce cas, multiplication par division. Les tests unitaires, en général, devraient détecter les erreurs lors de la modification des déclarations; sinon, ils ne testent pas efficacement le comportement du programme. C'est la logique à laquelle mutmut adhère lors de certaines modifications.
Nous pouvons utiliser la commande mutmut apply sur le mutant survivant. Wow, il s'avère que nous n'avons pas vérifié si le paramètre d'heure était correctement utilisé. Corrigeons ceci:
$ git diff
diff --git a/tests/test_angle.py b/tests/test_angle.py
index f51d43a..1a2e4df 100644
--- a/tests/test_angle.py
+++ b/tests/test_angle.py
@@ -2,3 +2,6 @@ import angle
def test_twelve():
assert angle.between(12, 00) == 0
+
+def test_three():
+ assert angle.between(3, 00) == 90
Auparavant, nous ne vérifiions que 12. L'ajout d'un chèque pour la valeur 3 sauverait la mise?
Ce nouveau test a réussi à tuer deux mutants: c'est mieux qu'avant, mais il reste encore du travail à faire. Je n'écrirai pas de solution pour chacun des 14 cas restants pour le moment, car l'idée est déjà claire (pouvez-vous tuer tous les mutants vous-même?)
En plus de mesurer la couverture, les tests de mutation vous permettent également d'évaluer la complétude de vos tests. De cette façon, vous pouvez améliorer vos tests: l'un des mutants survivants est une erreur que le développeur a peut-être commise, ainsi qu'un défaut potentiel de votre produit. Alors, je vous souhaite de tuer plus de mutants!
La publicité
VDSina propose des serveurs virtuels pour Linux et Windows - choisissez l'un des systèmes d'exploitation préinstallés ou installez à partir de votre image.