jeudi 16 février 2023

Testez vos scripts BASH avec TAP

Durant ma dernière mission, il m'a été demandé de fournir un logiciel prenant la forme de scripts BASH. Sans entrer dans les détails, je me suis très vite confronté à un problème: comment je teste ce que je fait ?

Criant à qui veut l'entendre que tout est testable, suffit de le vouloir; il était temps d'en faire la preuve … une fois de plus.

Après une demi-journée de recherche, voici à quoi je suis arrivé:

Test Anything Protocol
Ou: Comment tester n'importe quoi, et même du BASH 🙂

Des outils divers et variés

En réalité, entrer 'test' et 'bash' dans n'importe quel moteur de recherche conduit très vite à un résultat bien connu: BATS Une solution très intéressante que j'ai tout de même écartée pour deux raisons :

  • à cause de la syntaxe que je souhaitais ne pas introduire dans le projet pour rester sur du BASH «pure» et ainsi réduire le prérequis pour les personnes reprennant le projet
  • à cause de mon niveau de connaissance de l'époque, ne sachant pas à quel résultat je devais aboutir sur le logiciel en question, il était alors plus simple pour moi, à ce moment précis, de me concentrer sur le livrable et moins sur l'apprentissage d'une nouvelle techno

Cependant, j'ai gardé en tête ce TAP: Test Anything Protocol Il s'agit d'un contrat d'interface entre un producteur (une suite de tests) rapportant le résultat d'exécution de tests à un consommateur capable d'en faire quelque chose (formaliser en XML, reporting plus élaborer, …) et quite à partir sur une solution "maison", autant se rapprocher d'une convention potentiellement utilisable dans le workflow d'une CI.

❓Le saviez-vous❓
ShellCheck est un outil indispensable pour écrire des scripts BASH de qualité. En plus de forcer à uniformiser l'écriture d'un script, je ne compte plus le nombre de fois où il m'a indiqué une erreur dans le nom d'une variable avant même l'exécution des tests. Conclusion, ShellCheck est un ami qui vous veux du bien.

C'est décidé: j'écrits mon framework de tests BASH !

La spécification TAP est à la fois simple et claire, Y-A-PLU-KA!

Comment on s'en sert

Dans un cas simple:

# On importe la lib de test
source "test.shlib"

# On indique le résultat d'un test en succès
test_ok "this is a test success"

Ce qui devrait donner selon la spec TAP:

TAP version 14
ok 1 - this is a test success
1..1
  • La première ligne indiquant la version de la spec
  • S'en suit, les différents test préfixés par leur résultat, ici `ok 1 -` suivi d'un message associé au test
  • La dernière ligne indiquant la quantité de test attendu, ici `1..1` autrement dit 1/1

Dans le cas d'un échec:

# On importe la lib de test
source "test.shlib"

# On indique le résultat d'un test en échec
# petit bonus avec ce qu'on voulait / ce qu'on a eu
test_not_ok "this is a test failure" "true" "false"

Ce qui devrait donner selon la spec TAP:

TAP version 14
not ok 1 - this is a test failure
---
expecting: 'true'
got: 'false'
...
1..1

La touche du chef!

La spécification est bien plus vaste, mais ce sont là les 2 seules fonctionnalités qui vont m'être utiles.

J'ajoute à cela quelques fonctionnalités supplémentaires :

  • le test indiquera la ligne à laquelle il faudra se référer en cas d'erreur
  • en cas d'au moins un `not ok`, le test se terminera avec un code différent de 0 (zéro)
  • le test n'affichera aucun output si l'option --quiet est utilisée et à condition que tous les tests soient `ok`

Test-ception

C'est l'heure du Thé Dédé !

On teste le cas `ok` ...

# On importe la lib de test
source "${EXEC_ROOT}/test.shlib"

# Test `test_ok`
declare expected='TAP version 14
ok 1 - test_success.sh:10 - this is a test success
1..1'
declare actual
actual=$(src/test/resources/test_success.sh)
if [ "${expected}" == "${actual}" ]; then
test_ok "Can run successful tests"
else
test_not_ok "test.shlib MUST output successful test result" "${expected}" "${actual}"
fi
unset expected
unset actual
...

... puis on teste le cas `not ok`.

...
# Test `test_not_ok`
declare expected_out="TAP version 14
not ok 1 - test_failure.sh:10 - this is a test failure
---
expecting: 'true'
got: 'false'
...
1..1"
declare -i expected_exit_code=1
declare actual_out
set +o errexit
actual_out=$(src/test/resources/test_failure.sh)
declare -i actual_exit_code=$?
set -o errexit
if [ "${expected_out}" == "${actual_out}" ]; then
test_ok "Can run failed tests and display failed result"
else
test_not_ok "test.shlib MUST output failed test result" "${expected_out}" "${actual_out}"
fi
if [ "${expected_exit_code}" -eq ${actual_exit_code} ]; then
test_ok "Failing test exits in error"
else
test_not_ok "Failing test MUST exits in error" "${expected_exit_code}" "${actual_exit_code}"
fi
unset expected_out
unset expected_exit_code
unset actual_out
unset actual_exit_code

Comment ça marche ?

Comme on l'aura remarqué dans les tests précédents, il n'est pas qestion ici de fournir un tas d'assertions comme celles disponibles dans JUnit ou fournies par AssertJ.

L'idée ici est d'aller à l'essentiel, c'est à dire :

  • créer une situation initiale
  • agir
  • constater le changement
  • prévenir si le changement n'est pas celui recherché
  • nettoyer
  • recommencer

Comment c'est fait ?

Je ne rentre pas plus dans le détail ici, le code est publié sur un dépôt public. Il y a également le Makefile adéquat pour automatiser le tout et l'intégrer à une pipeline d'intégration continue.

Take-away

Si vous avez des scripts, il est à noter que ce sont des lignes de codes comme les autres et donc des lignes qui méritent leur propres tests.

Ce n'est pas si difficile: même si je recommande d'utiliser BATS (chose que je vais étudier plus en profondeur) il reste assez simple d'emprunter son propre chemin et de construire une solution de zéro en une heure maximum.

Aucun commentaire:

Enregistrer un commentaire