Environnement

ShUnit fonctionne évidemment sous unix mais aussi sous Windows avec cygwin. Pour développer du bash sous Unix, il suffit d'une console et d'un editeur texte quelconque. Les inconditionnels d'Eclipse pourront utiliser des plugins spécifiques comme ShellEd. Personnellement, je me suis prise d'affection pour vim car :
  1. il est souvent pré-installé et utilise peu de ressources
  2. il y a la coloration syntaxique ("syn on" dans ~/.vimrc) et l'autocomplétion (CTRL+P)
  3. il permet de visualiser cote à cote la fonction testée et le test (en mode commande : vsplit)
  4. il permet d'exécuter le test en tapant uniquement sur deux touches (en répétant la dernière commande executée : deuxPoints et flecheHaute), une fois qu'on a tapé une fois le script à lancer (:wall | ! clear && ./test_demo.sh). Le feedback est quasi immédiat.
bash en tdd

Et pourtant, à la base, mon expérience vi était plus que basique : je savais quitter et écrire dans un fichier. Un grand merci donc au binômage ! 

Installer ShUnit2

  1. Télécharger ShUnit2 et décompressez le dans un répertoire.
  2. Définissez éventuellement la variable d'environnement SHUNIT2_SCRIPTS sur votre machine, afin de faciliter les futures montées de version de la librairie :

    export SHUNIT2_SCRIPTS=/home/soat/workspace/agilesandbox/tutoShunit/shunit2-2.1.5/src/shell

Utiliser shUnit

Il suffit de sourcer le script contenant les fonctions de test : shunit2.

  1. . ${SHUNIT2_SCRIPTS}/shunit2

Les fonctions prefixées par le mot "test" seront exécutées comme des tests.

Premier test

Nous souhaitons implémenter un hello world à la française. La méthode saluer prendrait un nom en paramètre et saluerait cette personne. Conformément au test driven development, nous commençons par déclarer ces attentes dans un test unitaire, avant même d'implémenter la fonction. Le code retour attendu est zéro et nous devrions trouver un message amical personnalisé dans la sortie standard.


  1. #!/bin/bash
  2. test_saluer(){
  3. out=`saluer "Robert"`;
  4. assertEquals "code retour" "$?" "0"
  5. assertEquals "stdout" "Bonjour Robert, comment vas-tu ?" "${out}"
  6. }
  7. . ${SHUNIT2_SCRIPTS}/shunit2
Le lancement du script affiche :
test_saluer
./test_hello.sh: line 4: saluer : commande introuvable
ASSERT:code retour expected:<127> but was:<0>
ASSERT:stdout expected:<Bonjour Robert, comment vas-tu ?> but was:<>

Ran 1 test.

FAILED (failures=2)

le shell a retourné 1
Les erreurs sont visibles en guettant les "ASSERT". Les assertions échouent ici car la méthode n'est pas encore implémentée et surtout, nous n'avons pas sourcer les fonctions à tester. Cette opération doit être effectuée avant de lancer l'ensemble des tests.

La fonction oneTimeSetUp, fournie par shUnit, est justement appelée à ce moment là. Nous la redéfinissons en conséquence.

  1. oneTimeSetUp(){
  2. . ../main/hello.sh > /dev/null
  3. }

De manière générale, essayez au maximum d'épurer l'affichage des tests en redirigeant les sorties vers /dev/null, afin de rendre les erreurs plus visibles.

Nous implémentons maintenant la méthode afin de faire passer le test :

  1. saluer(){
  2. echo "Bonjour $?, comment vas-tu ?";
  3. }
Il passe !
test_saluer

Ran 1 test.

OK

Tests des sorties

Ecrivons un test pour afficher une erreur si aucun argument n'est passé en paramètre. Le code retour attendu est différent de 0.
  1. test_saluer_quandParametreManquant_messageDerreur(){
  2. out=`saluer`
  3. assertNotEquals "code retour" "$?" "0"
  4. assertEquals \
  5. "out" \
  6. "Argument manquant. Vous n'avez pas preciser qui saluer." \
  7. "${out}"
  8. }
Nous pouvons être plus précis en précisant que le message d'erreur doit être affiché dans la sortie erreur. Pour ce faire, nous allons rediriger les sorties standard et erreur dans des fichiers différents. 

Nous créons ces fichiers une fois avant l'exécution des tests dans le repertoire temporaire de shUnit et les supprimons à la fin.
 
  1. oneTimeSetUp()
  2. {
  3. . ../main/hello.sh > /dev/null
  4. outputDir="${__shunit_tmpDir}/output"
  5. mkdir -p "${outputDir}"
  6. stdout="${outputDir}/stdout"
  7. stderr="${outputDir}/stderr"
  8. }
  9. oneTimeTearDown(){
  10. rm -rf ${outputDir}
  11. }

  12. test_saluer_quandParametreManquant_erreurSurStderrEtSdoutVide(){
  13. saluer >${stdout} 2>${stderr}
  14. assertNotEquals "code retour" "$?" "0"
  15. assertNull "stdout" "`cat ${stdout}`"
  16. assertEquals "stderr" "Argument manquant. Vous n'avez pas preciser qui saluer." "`cat ${stderr}`"
  17. }

Autres fonctions

A côté d'assertEquals et assertNull, les autres fonctions les plus courantes de la suite xUnit sont elles aussi disponibles. Un message peut être spécifié en premier argument de chacune d'entre elles.

  1. test_pasDeFichierDeLog(){
  2. rm toto.log >/dev/null
  3. assertFalse 'il ne doit pas y avoir de fichier de log' "[ -f 'toto.log' ]"
  4. }
  5. test_fichierLogExiste(){
  6. echo "bonjour" > toto.log
  7. assertTrue 'il doit y avoir un fichier de log' "[ -f 'toto.log' ]"
  8. rm toto.log
  9. }
  10. test_a_faire(){
  11. fail "todo"
  12. }
  13. setUp(){
  14. echo "s'execute avant chaque test" >/dev/null
  15. }
  16. tearDown(){
  17. echo "s'execute apres chaque test" >/dev/null
  18. }
La méthode setUp est appelée avant chaque fonction de test tandis que tearDown après chacune d'entre elles.

Conclusion

Simple mais efficace, ShUnit2 fournit le nécessaire pour que même nos scripts shell soient écrits en TDD. Je l'utilise pour tester unitairement les fonctions mais aussi occasionnellement pour tester fonctionnellement un script de bout en bout.
Plus encore, il permet de créer un pont entre les équipes de développement et d'exploitation. 

Le seul obstacle que j'ai rencontré pour l'instant est un bug sur assertNotNull quand la variable testée contient une apostrophe. C'est peu par rapport au confort de ne plus avoir à tester manuellement la même chose plusieurs fois de suite et pour l'assurance que les tests apportent.