Comment tester efficacement vos scripts Bash

Testez les scripts Bash avec le mode strict, le traçage, Bats, shUnit2, les commandes simulées, les répertoires temporaires, ShellCheck et l'automatisation CI.

Comment tester efficacement vos scripts Bash

Les scripts Bash manipulent souvent des fichiers, des services, des déploiements et des données de production. Tester efficacement vos scripts Bash vous aide à détecter les mauvaises hypothèses avant qu'un nettoyage ne supprime le mauvais répertoire ou qu'un script de déploiement n'ignore une commande ayant échoué.

Vous n'avez pas besoin d'un framework imposant pour commencer. Combinez des options shell défensives, des vérifications statiques, des tests unitaires ciblés et des environnements de test temporaires pour que vos scripts échouent de manière bruyante et prévisible.


Fondamentaux : Codage défensif et débogage

Avant de mettre en œuvre des tests unitaires formels, la première couche de défense contre les bugs réside dans la structure même du script. L'utilisation de paramètres opérationnels stricts peut aider à transformer des erreurs d'exécution subtiles en échecs immédiats, facilitant ainsi leur débogage.

En-tête défensif essentiel

De nombreux scripts Bash de production commencent par des options plus strictes :

#!/bin/bash
# Quitte immédiatement si une commande se termine avec un statut non nul.
set -e

# Traite les variables non définies comme une erreur lors de la substitution.
set -u

# Empêche que les erreurs dans un pipeline soient masquées.
set -o pipefail

Combiner ces options en set -euo pipefail est courant. Soyez conscient que set -e a des cas particuliers dans les conditionnelles, les sous-shells et les pipelines, donc vérifiez toujours explicitement les échecs attendus au lieu de supposer que le mode strict remplace les tests.

Débogage manuel avec le traçage

Pour un débogage rapide ou pour comprendre le flux d'exécution d'un script, Bash offre des capacités de traçage intégrées :

  • Traçage des commandes (-x) : Affiche les commandes et leurs arguments au fur et à mesure de leur exécution, précédés de +.
  • Pas d'exécution (-n) : Lit les commandes mais ne les exécute pas (utile pour vérifier les erreurs de syntaxe).

Vous pouvez activer le traçage soit lors de l'exécution du script, soit à l'intérieur du script lui-même :

# Exécution du script avec traçage
bash -x ./mon_script.sh

# Activation du traçage dans le script pour une section spécifique
echo "Début de l'opération complexe..."
set -x # Active le traçage
complex_function_call arg1 arg2
set +x # Désactive le traçage
echo "Opération terminée."

Adoption de frameworks de tests unitaires formels

Le débogage manuel n'est pas viable pour une logique complexe. Les frameworks de tests unitaires formels vous permettent de définir des cas de test reproductibles, d'affirmer les résultats attendus et d'automatiser le processus de validation.

1. Bats (Bash Automated Testing System)

Bats est sans doute le framework le plus populaire et le plus simple pour les tests Bash. Il permet d'écrire des tests en utilisant une syntaxe Bash familière, rendant les assertions simples et lisibles.

Fonctionnalités clés de Bats :

  • Les tests sont écrits avec une syntaxe de type Bash.
  • Utilise la commande simple run pour exécuter le script/la fonction cible.
  • Fournit des variables d'assertion intégrées comme $status, $output et $lines.

Exemple : Tester une fonction simple

Imaginez que vous ayez un script (calculator.sh) contenant une fonction calculate_sum.

Extrait de calculator.sh :

calculate_sum() {
  if [[ $# -ne 2 ]]; then
    echo "Erreur : Nécessite deux arguments" >&2
    return 1
  fi
  echo $(( $1 + $2 ))
}

test/calculator.bats :

#!/usr/bin/env bats

# Source le script contenant les fonctions à tester.
# BATS_TEST_DIRNAME pointe vers le répertoire contenant ce fichier de test.
source "$BATS_TEST_DIRNAME/../calculator.sh"

@test "Les entrées valides doivent retourner la somme correcte" {
  run calculate_sum 10 5
  # Affirme que la fonction a retourné un statut de succès (0)
  [ "$status" -eq 0 ]
  # Affirme que la sortie correspond à l'attente
  [ "$output" = "15" ]
}

@test "Les entrées manquantes doivent retourner un statut d'erreur (1)" {
  run calculate_sum 5
  [ "$status" -ne 0 ]
  [ "$status" -eq 1 ]
  # Dans les versions récentes de bats-core, stderr est disponible lors de l'utilisation de `run`.
  # [ "$stderr" = "Erreur : Nécessite deux arguments" ] 
}

Pour exécuter les tests :

bats test/calculator.bats

2. ShUnit2

ShUnit2 suit le style de test xUnit, ce qui le rend familier aux développeurs venant de langages comme Python ou Java. Il nécessite de sourcer les fichiers du framework et adhère à une convention de nommage stricte (setUp, tearDown, test_...).

Fonctionnalités clés de ShUnit2 :

  • Prend en charge les routines de configuration et de nettoyage.
  • Fournit un ensemble riche de fonctions d'assertion intégrées (par exemple, assertTrue, assertEquals).

Structure ShUnit2

#!/bin/bash
# Source shUnit2. Ajustez ce chemin pour votre installation.
. /usr/local/share/shunit2/shunit2

# Définir les variables/fixtures

setUp() {
  # Code à exécuter avant chaque test
  TEMP_FILE=$(mktemp)
}

tearDown() {
  # Code à exécuter après chaque test (nettoyage)
  rm -f "$TEMP_FILE"
}

test_basic_addition() {
  local result
  # Appel de la fonction testée
  result=$(my_script_function 1 2)
  
  # Utilisation d'une fonction d'assertion
  assertEquals "3" "$result"
}

# Si votre paquet shUnit2 attend un sourçage explicite à la fin,
# sourcez-le après vos fonctions de test plutôt que près du début.

Meilleures pratiques pour les tests de scripts Bash

Des tests efficaces vont au-delà de l'exécution d'un framework ; ils nécessitent un isolement minutieux des composants et une gestion des dépendances environnementales.

1. Gestion des entrées, sorties et erreurs

Vos tests doivent vérifier les flux standard (stdout, stderr) et le code de sortie final, qui est le mécanisme principal pour signaler le succès ou l'échec dans Bash.

  • Codes de sortie : Testez status -eq 0 pour le succès et les valeurs non nulles pour les conditions d'erreur telles que l'échec d'analyse ou les fichiers manquants.
  • Sortie standard (stdout) : C'est généralement la sortie de données principale. Utilisez $output de Bats ou capturez la sortie dans ShUnit2 pour affirmer l'exactitude.
  • Erreur standard (stderr) : Les erreurs, avertissements et messages de débogage doivent être dirigés ici. Crucialement, assurez-vous que les scripts de production sont silencieux sur stderr lors des exécutions réussies.

2. Isolement des dépendances (Simulation)

Les tests unitaires doivent tester votre code, pas les outils système externes (comme curl, kubectl ou git). Si votre script dépend d'une commande externe, vous devez simuler cette commande pendant le test.

Méthode : Créez un répertoire temporaire contenant des fichiers exécutables simulés qui ont le même nom que les vraies dépendances. Ajoutez ce répertoire à votre $PATH avant d'exécuter le test, garantissant que votre script appelle la simulation au lieu de l'outil réel.

Exemple de simulation :

#!/bin/bash
# Fichier : /tmp/mock_bin/curl

if [[ "$1" == "--version" ]]; then
  echo "Mock Curl 7.6"
  exit 0
else
  # Simule une réponse API réussie
  echo '{"status": "ok"}'
  exit 0
fi

Dans votre configuration de test :

export PATH="/tmp/mock_bin:$PATH"

3. Tests d'intégration avec des environnements temporaires

Les tests d'intégration vérifient que le script interagit correctement avec le système de fichiers et le système d'exploitation. Utilisez des répertoires temporaires pour éviter de polluer le système ou d'interférer avec d'autres tests.

Utilisation de mktemp

La commande mktemp -d crée un répertoire temporaire sécurisé et unique. Vous devez effectuer toutes les manipulations de fichiers (création, modification, nettoyage) dans ce répertoire pendant l'exécution du test.

setUp() {
  # Crée un répertoire temporaire pour cette exécution de test
  TEST_ROOT=$(mktemp -d)
  cd "$TEST_ROOT"
}

tearDown() {
  # Nettoie le répertoire temporaire
  cd - >/dev/null
  rm -rf "$TEST_ROOT"
}

@test "Le script doit créer le fichier journal requis" {
  run my_script_that_writes_logs
  
  # Affirme que le fichier attendu existe dans le répertoire temporaire
  [ -f "./log/script.log" ]
}

4. Test de portabilité

Les implémentations de Bash varient légèrement (par exemple, GNU Bash vs. macOS/BSD Bash). Si la portabilité est une préoccupation, exécutez votre suite de tests sur divers environnements cibles (par exemple, en utilisant des conteneurs Docker) pour détecter les différences subtiles dans les commandes utilitaires ou l'expansion des paramètres.

Intégration des tests dans le flux de travail

Les tests ne doivent pas être une réflexion après coup. Incorporez votre suite de tests dans votre contrôle de version et votre pipeline CI/CD (Intégration Continue/Déploiement Continu).

  1. Contrôle de version : Stockez le répertoire de test (par exemple, test/) à côté de vos scripts source.
  2. Hooks de pré-commit : Utilisez des outils comme shellcheck (un outil d'analyse statique) et des formateurs pour garantir la qualité du code avant les commits.
  3. Automatisation CI : Configurez votre serveur CI (GitHub Actions, GitLab CI, Jenkins) pour exécuter automatiquement la suite de tests Bats ou ShUnit2 à chaque push. Échouez la construction si un test retourne un statut non nul.

Avertissement : Les outils d'analyse statique comme shellcheck sont d'excellents compagnons pour les tests unitaires. Ils détectent les erreurs courantes, les problèmes de portabilité et les vulnérabilités de sécurité que les tests pourraient manquer. Exécutez toujours shellcheck dans le cadre de votre routine de pré-test.

Conclusion

Commencez avec shellcheck et set -euo pipefail, puis ajoutez des tests autour des parties de votre script qui analysent les entrées, choisissent des fichiers, appellent des outils externes ou effectuent des modifications irréversibles. Une petite suite Bats avec des dépendances simulées et des répertoires temporaires suffit souvent pour transformer un script risqué en une automatisation que vous pouvez modifier en toute confiance.