Si comme moi vous pratiquez le test driven development, vous lancez logiquement vos tests très souvent. Plusieurs centaines de fois par jour même. Or sur certains projets, l'exécution des tests peut prendre un certain temps. Il est donc important d'optimiser cela très tôt dans un projet.
Je présente ici quelques pistes pour faire cela.
Cet article concerne principalement le langage Elixir, dont le framework de
test, ExUnit
, est un petit bijou. Les principes décrits ici sont applicables à
d'autres langages ou frameworks mais cela demandera plus de travail.
Si votre langage ou framework le permet, la première chose à faire est de trouver comment lancer uniquement les tests modifiés depuis le dernier lancement des tests.
La commande mix test --stale
est assez magique.
Elle permet de ne lancer que les tests qui touchent du code modifié. Si, après
avoir lancé les tests, vous modifiez uniquement un module A
, alors seuls les
tests exécutant du code de ce module seront lancés.
Le tracking est bien sûr transitif, si le module A
appelle du code de B
et
que vous avez modifé B
alors les tests pour A
seront lancés, comme les tests
pour le module B
.
Si votre code est bien découplé, l'exécution des tests sera très courte.
Et bien sûr, si vous modifiez un test, il sera relancé également.
Le fameux cycle red-green-refactor implique qu'au moins un test va échouer pour trois exécutions de la suite de tests. En théorie du moins. En pratique ce sera bien plus souvent.
Il serait donc intéressant de pouvoir relancer uniquement le test qui a échoué.
La command mix test --failed
permet de faire cela.
Mon workflow est donc le suivant :
mix test --stale
.mix test --failed
et modifier l'implémentation plusieurs fois
jusqu'à ce que le test soit green.mix test --stale
jusqu'à ce qu'on soit satisfait du
code et que le test soit toujours green. Si un test est red (cela peut
être un autre test), nous nous sommes en fait téléportés au point 4
ci-dessus.Ça reste assez simple et intuitif : --stale
pour lancer les tests quand ils
sont green et --failed
pour lancer les tests quand ils sont red.
Nous allons encore simplifier tout ça. Voici notre nouvelle commande de test:
mix test --failed && mix test --stale
Seulement, il reste un problème.
Au point 5, il se peut qu'une de nos modifications ait cassé plusieurs autres tests. Des dizaines même. Il faut donc les corriger un par un.
La première chose à ajouter à notre commande est l'option --max-failures 1
qui
permet de s'arrêter dès la première erreur, afin de ne pas avoir un ouput en
console illisible, et de savoir rapidement quoi corriger.
Cela donne ça:
mix test --failed --max-failures 1 && \
mix test --stale
Malheureusement, cela ne suffit pas. Si plusieurs tests échouent, la commande
mix test --failed --max-failures 1
va s'arrêter sur n'importe quel test.
La raison est que par défaut, mix test
lance tous les tests dans un ordre
aléatoire.
:async
qui ne compliquent pas vraiment le problème.)Il existe une solution mais les choses vont se compliquer un peu.
Nous aimerions lancer le même test encore et encore, jusqu'à ce qu'il soit green, avant de passer au test suivant.
Pour remédier à cela, il est possible de passer un integer à l'option --seed
de mix test
, qui permet de fixer la graine du générateur aléatoire et de
présever l'ordre des tests :
mix test --failed --max-failures 1 --seed 1234 && \
mix test --stale --seed 1234
La seed étant fixe, tous nos tests s'exécuteront toujours dans le même ordre. Mais il y a une raison pour laquelle les tests sont lancés dans un ordre aléatoire ! Cela permet de détecter de mauvais tests qui fonctionnent uniquement parce qu'un autre test à modifié l'état de l'application et a créé une certaine condition. Si cet autre test n'est pas exécuté avant, ces mauvais tests ne fonctionneront plus.
Pour garder cette fonctionnalité, nous allons donc modifier la commande afin de générer une nouvelle seed après chaque exécution complète de la commande. Tant qu'au moins un test est red on garde la même seed, et quand tout est green on change de seed.
Il y a plusieurs façons de faire ça, mais voici une méthode facile. La plupart
des shells sur Linux (et probablement MacOS) définissent la variable $RANDOM
qui contient un nombre aléatoire entre 0 et 32767.
Exécutez ceci et vous aurez trois nombres différents :
echo $RANDOM
echo $RANDOM
echo $RANDOM
Pour sauvegarder notre seed et la réutiliser plusieurs fois, nous alons
l'écrire dans un fichier. Nous le plaçons dans le dossier _build
qui contient
tous les artifacts de compilation et de test.
Nous devons également gérer le cas où le fichier n'existe pas encore. Dans ce cas nous utilisons une seed par défaut.
Voici donc notre nouvelle commande:
mix test --failed --max-failures 1 --seed $(cat _build/seed || echo 0) && \
mix test --stale --seed $(cat _build/seed || echo 0) && \
echo $RANDOM > _build/seed
Lors de la première exécution, le fichier seed
n'existant pas, cat
affichera un message d'erreur que nous pouvons ignorer, ou supprimer en
exécutant:
touch _build/seed
Pour le moment, cette commande est un peu longue à taper, on voudra en général la stocker quelque part. Je présente une solution dans la seconde partie.
Mais elle permet de prioriser l'exécution des tests, de rester concentré sur un seul problème à la fois et de ne pas s'éparpiller.
Je vous incite donc à l'essayer et bien sûr de la personnaliser à votre convenance.
J'aimerais aussi vous parler de odo qui est un simple programme écrit en C permettant tout simplement d'incrémenter un nombre dans un fichier. Je l'utilise simplement pour aérer un peu ma commande :
mix test --failed --max-failures 1 --seed $(odo -c _build/seed.txt) && \
mix test --stale --seed $(odo -c _build/seed.txt) && \
odo _build/seed.txt
Pour l'installer, il faut cloner et le compiler, et supprimer le fichier seed
existant. En soi rien de bien compliqué mais j'ai préféré présenter la solution
avec $RANDOM
qui n'a pas de dépendance et est immédiatement disponible.
Bons tests !