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 :
- il est souvent pré-installé et utilise peu de ressources
- il y a la coloration syntaxique (“syn on” dans ~/.vimrc) et l’autocomplétion (
CTRL+P
) - il permet de visualiser cote à cote la fonction testée et le test (en mode commande :
vsplit
) - il permet d’exécuter le test en tapant uniquement sur deux touches (en répétant la dernière commande executée :
deuxPoints
etflecheHaute
), une fois qu’on a tapé une fois le script à lancer (:wall | ! clear && ./test_demo.sh
). Le feedback est super rapide !
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
- Télécharger ShUnit2 et décompressez le dans un répertoire.
- 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.