Workflow TDD – Partie 2

Optimisez votre dev loop !

8 décembre 2023 ⋅ #TDD #Elixir

Dans un précédent article, j'ai présenté ma solution pour définir une seule commande qui sélectionne automatiquement les tests à lancer en fonction de l'état actuel des tests.

Voyons maintenant comment lancer ça par la pensée. Ou presque.

Avant-propos

Je ne sais pas si vous avez fait du développement web avant la généralisation du hot reload mais si c'est le cas, il y a une suite de commandes que vous avez du taper de très nombreuses fois:

Ctrl+S Alt+Tab Ctrl+R

Je sauve mon code, je vais dans le navigateur, je recharge … mille fois par jour !

À force, on se rend compte que le cerveau traite ça comme une seule action. On ne décompose plus les différents gestes, la mémoire musculaire fait son boulot et il suffit de penser « voir le résultat » pour que nos mains s'activent et que le navigateur recharge la page.

J'ai depuis perdu cette habitude, travaillant principalement avec un terminal comme seul output, mais j'ai voulu conserver ce principe de boucle.

Je vous présente donc mon setup actuel dans Visual Studio Code, mais il est possible de faire la même chose avec n'importe quel éditeur capable d'intégrer un terminal comme Neovim ou Emacs.

Faire de la place …

Bon déjà, faites-moi le plaisir de mettre votre terminal VSCode sur le côté. L'avoir en dessous de la page bouffe une place pas possible!

Le code c'est vertical, il faut pouvoir le voir en entier. Vous écrivez peut-être du code parfait avec un maximum de trois lignes par fonction, mais ça n'est pas le cas de vos collègues ;)

Et le terminal non plus n'a pas besoin d'afficher des lignes de 200 caractères.

Lancer une commande dans le terminal de VSCode via un raccourci

Nous allons configurer un raccourci pour lancer une commande dans le terminal de VSCode sans avoir à aller dans le terminal. On peut donc écrire du code, lancer la commande et continuer d'écrire sans avoir à gérer le focus du curseur.

Notez que le raccourci fonctionne aussi quand le curseur est dans le terminal.

J'ai choisi la combinaison Ctrl+Alt+U. « U » pour Unit Test puisque en général cette commande me sert à lancer les tests. Mais vous mettez évidemment ce que vous voulez.

Voici comment modifier les raccourcis dans VSCode:

  • Faites Ctrl+Shift+P pour ouvrir la command palette.
  • Tapez « shortcuts » et sélectionnez « Preferences: Open Keyboard Shortcuts (JSON) ».
  • Ajoutez le code de configuration. Pensez-bien à terminer le texte de la commande par \n pour exécuter l'action dans le terminal.

Et voici le code à ajouter. Ici j'ai repris la commande présentée dans mon précédent article.

{
  "key": "ctrl+alt+u",
  "command": "workbench.action.terminal.sendSequence",
  "args": {
    "text": "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\n"
  }
},

Ce n'est pas très lisible, mais on va arranger ça.

Un nouveau workflow

À partir de maintenant, le workflow de test devient :

  • Écrire un bout de code.
  • Sauvegarder.
  • Faire Ctrl+Alt+U.

Et bien sûr gérer les cinquante autres problèmes qui surviennent pendant une session de code, mais c'est une autre histoire.

Vu que la commande de test va automatiquement lancer les tests que vous voulez, il n'y a plus besoin de réfléchir à quoi exécuter. (Dans 90% des cas… Il y a toujours des exceptions, c'est un peu la base de notre métier.)

Cette alternance entre écrire du code et vérifier le résultat se retrouve tout le temps, dans quasiment tous les projets. Et pas uniquement pour les tests. Par exemple quand on écrit de la documentation en markdown, quand on travaille sur un script de migration qui n'est pas vraiment testable, quand on s'arrache les cheveux sur un problème d'Advent Of Code, quand on travaille une commande cURL un peu velue, etc.

On travaille généralement dans une boucle, et avoir une commande unique pour lancer cette boucle est une bonne habitude à prendre pour qu'exécuter le code très souvent devienne un réflexe. Coder pendant 10 minutes sans rien tester est une mauvaise idée.

Et là vous me dites « tu es bien gentil, mais ta commande elle lance des tests Elixir, ça ne fonctionne pas avec mon Python pour AoC ou mon script bash pour générer mon site statique ».

Oui. C'est un problème, lancer les tests c'est cool mais on voudrait aussi pouvoir faire autre chose.

Utiliser une commande différente pour chaque projet

La solution pour ça est assez facile: On modifie le raccourci VSCode pour qu'il exécute un script, et ce script peut être différent pour chaque projet.

Lors de l'initialisation d'un projet, je crée un fichier dev-loop à la racine du projet:

touch dev-loop
chmod +x dev-loop

Après avoir copié la commande dans le fichier, il suffira d'exécuter ./dev-loop pour l'exécuter.

Et voici le contenu de mon fichier, Avec la commande de test pour Elixir:

#!/usr/bin/bash -ve
trap exit INT
mix test --failed --max-failures 1 --seed $(cat _build/test-seed || echo 0)
mix test --stale --seed $(cat _build/test-seed || echo 0)
echo $RANDOM > _build/test-seed

Quelques notes:

  • Le script sera exécuté avec /usr/bin/bash quelque soit votre shell.
  • L'option -v permet d'afficher les commandes exécutées. Je ne sais toujours pas si je préfère avec ou sans!
  • Plus intéressante, l'option -e permet d'arrêter le script dès qu'une commande renvoie un code d'erreur. Cela permet de lister les commandes indépendamment, enlève les && \ un peu moches, et laisse la possibilité d'ajouter ou de commenter des lignes plus facilement.
  • La commande trap exit INT permet au Ctrl+C d'arrêter tout le script quand on interrompt l'exécution d'une des commandes (ce qui ne renvoie pas un code d'erreur).

Et on peut donc l'appeler avec notre raccourci :

{
  "key": "ctrl+alt+u",
  "command": "workbench.action.terminal.sendSequence",
  "args": {
    "text": "./dev-loop\n"
  }
},

Gardez ce fichier pour vous !

Le fichier étant créé dans un projet, il risque d'être commité dans Git. Ici plusieurs solutions :

  • Vous arrivez à convaincre vos collègues de l'intérêt de ce fichier et vous le gardez dans Git. Mais vous ne pouvez plus le modifier à votre convenance.
  • Vous l'ajoutez dans .gitignore mais il faudra expliquer en boucle de quoi il s'agît car personne ne verra jamais le fichier apparaître.
  • Ma solution préférée : vous ajoutez une ligne contenant seulement dev-loop dans votre fichier .gitignore global (~/.gitignore) et vous pouvez ainsi créér dev-loop dans n'importe quel projet Git sans souci.

Utiliser plusieurs commandes dans un même projet

Le fait d'utiliser un fichier améliore pas mal de choses mais il reste un problème: après avoir fait une session TDD de quelques heures, on aimerait facilement changer de boucle, passer sur de la doc. Il faudrait que notre commande fasse temporairement autre chose afin de bénéficier de notre workflow bien pratique.

On pourrait imaginer plein de solutions, comme par exemple avoir plusieurs raccourcis, ou modifier une variable d'environnement via un script qui affiche un prompt… trop compliqué!

Je préfère avoir un setup modifiable et robuste. Il y aura toujours des douzaines de commandes à entrer manuellement de toutes façons.

Pour pallier cela, je définis des fonctions dans mon fichier dev-loop:

#!/usr/bin/bash -e
trap exit INT

# task=dev
# task=dialyzer
task="${task:-test}"

task_dev()
{
  mix run tmp/some-script.exs
}

task_dialyzer()
{
  task_test
  mix dialyzer
}

task_test()
{
  mix test --failed --max-failures 1 --seed $(odo -c _build/seed.txt) --trace
  mix test --stale --seed $(odo -c _build/seed.txt)
  odo _build/seed.txt
}

task_$task

Je peux donc décommenter temporairement l'une de ces deux lignes pour changer de boucle:

# task=dev
# task=dialyzer

Et comme on est en bash, on peut en fait faire une infinité de choses. Ici par exemple, très simple, la tache qui exécute Dialyzer lance également les tests avant.

Alternatives

Il existe cependant d'autres possibilités pour exécuter une commande spécifique à chaque projet :

  • Utiliser les outils de build/run de l'éditeur. Je n'ai jamais pris l'habitude de faire ça mais je suppose qu'on peut avoir un résultat similaire. Je préconise quand même de configurer le run pour qu'il exécute un script, car le script est indépendant.

  • Utiliser un mode watch de votre outil de test. Il en existe un pour Elixir/ExUnit. Je n'ai pas vraiment trouvé cela intéressant. Sur le papier ça fonctionne bien, en réalité c'est vite frustrant car ça ne lance pas ce que je veux. Et cela garde le terminal occupé.

  • Utiliser des scripts globaux. Ma commande de test est quasiment la même sur tous les projets, donc cela pourrait être un script global qui s'exécute dans le dossier courant. Avec un autre script pour la doc, un autre script pour les projets JS, Python, etc.. Pourquoi pas!

Ce que j'aime avec l'utilisation d'un fichier c'est que ça continue de fonctionner même dans un environnement de travail dégradé. Par exemple à distance sur un serveur, ou avec un autre éditeur ; il suffit de lancer ./dev-loop à la main, tant pis pour le raccourci.

Ou encore quand justement on travaille sur un script bash ou .exs, ou sur une grosse commande cURL: I l suffit de mettre la commande dans le fichier.

C'est totalement adaptable à n'importe-quelle situation.

Oh et bien sur on peut utiliser des variables d'environnement ou passer des arguments au script.

Conclusion

Ce setup assez rapide à mettre en place permet d'avoir une commande personnalisée facilement modifiable pour chaque projet, propre à vous.

Une fois que votre mémoire musculaire se sera faite, vous la lancerez automatiquement en pensant « je veux voir si ça marche » … à condition d'avoir les mains sur le clavier.

Personnellement je ne peux plus m'en passer !

Raccourcis bonus

Voici comment configurer VSCode pour sauter entre le terminal et le code:

{
  "key": "ctrl+l",
  "command": "workbench.action.terminal.focus"
},
{
  "key": "ctrl+l",
  "command": "workbench.action.focusActiveEditorGroup",
  "when": "terminalFocus"
},

Et pour Elixir uniquement, voici comment lancer uniquement le test dans lequel se trouve le curseur, et tout les tests du fichier en cours :

{
  "key": "alt+u",
  "command": "workbench.action.terminal.sendSequence",
  "args": {
    "text": "mix test ${file}:${lineNumber}\n"
  }
},
{
  "key": "shift+alt+u",
  "command": "workbench.action.terminal.sendSequence",
  "args": {
    "text": "mix test ${file} --trace\n"
  }
},