Accueil Nos publications Blog Tester des scripts shell

Tester des scripts shell

Originellement développé par Vera Peeters and Rik Tytgat, ShUnit a été le premier outil de tests unitaires en shell. Il vous permet de valider que vos scripts shell effectuent bien ce que vous en attendez. Intégrez vos tests shunit à votre usine de développement et vous serez assurés qu’ils resteront valides de façon permanente, qu’ils ne souffriront d’aucune régression sans vous en alerter.

Je vous propose de me suivre sur les traces d’un framework un peu plus xUnit like : ShUnit2 de Kate Ward.

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 super rapide !

TDD en bash

Pourtant,  à la base mon expérience vi était plus que basique : je savais quitter et écrire dans un fichier. Merci le 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.

. ${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.

#!/bin/bash

test_saluer(){
out=`saluer "Robert"`;
assertEquals "code retour" "$?" "0"
assertEquals "stdout" "Bonjour Robert, comment vas-tu ?" "${out}"
}

. ${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 évidemment, car la méthode n’est pas encore implémentée et surtout, nous n’avons pas sourcer les fonctions à tester. Nous souhaitons le faire 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.


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

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 la méthode afin de faire passer le test :


saluer(){
echo "Bonjour $?, comment vas-tu ?";
}

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.


test_saluer_quandParametreManquant_messageDerreur(){
out=`saluer`
assertNotEquals "code retour" "$?" "0"
assertEquals \
"out" \
"Argument manquant. Vous n'avez pas preciser qui saluer." \
"${out}"
}

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.


oneTimeSetUp()
{
. ../main/hello.sh >/dev/null

outputDir="${__shunit_tmpDir}/output"
mkdir -p "${outputDir}"
stdout="${outputDir}/stdout"
stderr="${outputDir}/stderr"
}

oneTimeTearDown(){
rm -rf ${outputDir}
}

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

Autres fonctions

En plus des assertEquals et assertNull, les autres fonctions les plus courantes de la suite xUnit sont eux aussi disponibles. Un message peut être spécifié en premier argument de chacune d’entre elles.


test_pasDeFichierDeLog(){
rm toto.log >/dev/null
assertFalse 'il ne doit pas y avoir de fichier de log' "[ -f 'toto.log' ]"
}

test_fichierLogExiste(){
echo "bonjour" > toto.log
assertTrue 'il doit y avoir un fichier de log' "[ -f 'toto.log' ]"
rm toto.log
}

test_a_faire(){
fail "todo"
}

setUp(){
echo "s'execute avant chaque test" >/dev/null
}

tearDown(){
echo "s'execute apres chaque test" >/dev/null
}

Simple mais efficace, ShUnit fournit le nécessaire pour que même nos scripts shell soient écrits en TDD. Plus encore, il permet de créer un pont entre les équipes de développement et d’exploitation.

Je l’utilise pour tester unitairement les fonctions mais aussi occasionnellement pour tester fonctionnellement un script de bout en bout. Le seul obstacle 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 la confiance que les tests donnent.