0Pricing
DevOps Bootcamp · Leçon

Dispositifs de test, environnements temporaires et couverture

Créez des dispositifs de test isolés et mesurez les branches de vos scripts réellement parcourues par vos tests.

Dispositifs de test, environnements temporaires et couverture est une leçon DevOps Bootcamp gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DevOps Bootcamp, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DevOps Bootcamp comprend 4 leçons au total.

Pourquoi les dispositifs de test et l'isolation sont importants

Lors des essais de scripts Bash, le plus grand risque vient des effets secondaires : vos essais modifient accidentellement de vrais fichiers, de vraies bases de données ou l'état réel du système. Un essai qui réussit sur votre machine mais corrompt les données de production est pire que l'absence totale d'essai.

La solution consiste à utiliser des dispositifs de test : des environnements contrôlés et temporaires qui reproduisent les conditions réelles sans toucher aux éléments réels. De bons dispositifs vous offrent :

  • Reproductibilité — les essais produisent le même résultat à chaque exécution
  • Isolation — les essais n'interfèrent pas entre eux ni avec la machine hôte
  • Sécurité — les opérations destructrices ne portent que sur des données jetables
  • Rapidité — aucun appel réseau ni opération d'entrée-sortie lourde, sauf nécessité absolue

Pour les essais Bash, les dispositifs sont généralement des répertoires temporaires contenant des fichiers connus, des exécutables simulés placés au début de $PATH et des variables d'environnement limitées au processus d'essai.

Créer et nettoyer des répertoires temporaires

Le modèle standard pour créer un répertoire temporaire propre à chaque essai utilise mktemp -d, qui crée un répertoire unique dans /tmp et en affiche le chemin. Vous mémorisez ce chemin et enregistrez un trap pour le supprimer automatiquement à la fermeture du shell, même en cas d'échec.

Cette construction en deux lignes doit apparaître dans chaque fichier d'essai qui manipule le système de fichiers :

#!/usr/bin/env bash
set -euo pipefail

# Create an isolated temp directory
TMPDIR=$(mktemp -d)
# Always clean up, even if the script exits early or errors out
trap 'rm -rf "$TMPDIR"' EXIT

echo "Working in: $TMPDIR"

# Simulate creating fixture files
mkdir -p "$TMPDIR/project/{src,tests,logs}"
echo 'version=1.2.3' > "$TMPDIR/project/.env"
echo 'Hello fixture' > "$TMPDIR/project/src/main.sh"

ls -R "$TMPDIR/project"
echo 'Temp dir will be removed automatically on exit'

Structurer l'arborescence d'un dispositif de test

Un dispositif bien structuré reproduit l'organisation des répertoires réellement attendue par votre script soumis à l'essai. Considérez-le comme une racine de projet factice miniature. La fonction de préparation du dispositif crée cette organisation avant chaque essai, et le nettoyage la supprime.

Bonnes pratiques :

  • Utilisez une fonction setup() appelée par votre cadre de test avant chaque essai
  • Utilisez une fonction teardown() ou cleanup() exécutée après chaque essai, même en cas d'échec
  • Gardez les fichiers du dispositif minimaux : uniquement ce que le script lit réellement
  • Donnez aux fichiers du dispositif des noms descriptifs afin de faciliter le diagnostic des échecs
#!/usr/bin/env bash
# fixture_helpers.bash — source this from your test files

FIXTURE_ROOT=''

setup_fixture() {
  FIXTURE_ROOT=$(mktemp -d)
  # Build the directory tree the deploy script expects
  mkdir -p "$FIXTURE_ROOT"/{dist,config,logs}
  echo '{"version":"2.0"}' > "$FIXTURE_ROOT/config/app.json"
  echo 'console.log("app")' > "$FIXTURE_ROOT/dist/index.js"
  touch "$FIXTURE_ROOT/logs/.gitkeep"
  export FIXTURE_ROOT
  echo "[setup] Fixture ready at $FIXTURE_ROOT"
}

teardown_fixture() {
  if [[ -n "$FIXTURE_ROOT" && -d "$FIXTURE_ROOT" ]]; then
    rm -rf "$FIXTURE_ROOT"
    echo '[teardown] Fixture removed'
  fi
}

# Self-test
setup_fixture
ls "$FIXTURE_ROOT"
teardown_fixture

Simuler des exécutables avec un PATH factice

De nombreux scripts Bash appellent des outils externes comme curl, aws, docker ou git. Lors des essais, vous ne voulez pas contacter de vrais services ; vous remplacez donc ces outils par des exécutables factices.

La technique est simple :

  1. Créez un répertoire temporaire bin/ à l'intérieur de votre dispositif
  2. Écrivez-y de petits scripts shell portant les mêmes noms que les vrais outils
  3. Ajoutez ce répertoire au début de $PATH avant d'appeler votre script soumis à l'essai

Comme $PATH est parcouru de gauche à droite, le fichier factice est choisi en premier. Le véritable binaire n'est jamais appelé.

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Create a fake 'curl' that records calls and returns canned data
FAKE_BIN="$FIXTURE/bin"
mkdir -p "$FAKE_BIN"

cat > "$FAKE_BIN/curl" << 'SCRIPT'
#!/usr/bin/env bash
# Log every argument for later inspection
echo "curl $*" >> "$FIXTURE_BIN_LOG"
# Return a canned HTTP 200 response body
echo '{"status":"ok","id":42}'
SCRIPT
chmod +x "$FAKE_BIN/curl"

# Export the log path so the fake can find it
export FIXTURE_BIN_LOG="$FIXTURE/curl_calls.log"

# Prepend fake bin directory to PATH
export PATH="$FAKE_BIN:$PATH"

# Now any call to 'curl' hits our fake
curl -s https://api.example.com/health
curl -X POST https://api.example.com/deploy

echo '--- Recorded curl calls ---'
cat "$FIXTURE_BIN_LOG"

Capturer et vérifier la sortie des commandes

Un dispositif n'est utile que si vous pouvez vérifier ce que votre script a fait. Les modèles standard sont les suivants :

  • Capturer stdout/stderr dans des variables avec $() ou une substitution de processus
  • Examiner les fichiers journaux écrits par les binaires factices
  • Vérifier explicitement les codes de sortie avec $? ou une logique conditionnelle
  • Vérifier que certains fichiers ont été créés, modifiés ou laissés intacts

Écrire de petits assistants de vérification ciblés rend vos essais lisibles et fournit des messages d'échec précis lorsqu'un problème survient.

#!/usr/bin/env bash
set -euo pipefail

# Minimal assertion helpers
assert_eq() {
  local desc="$1" expected="$2" actual="$3"
  if [[ "$expected" == "$actual" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc"
    echo "  expected: $expected"
    echo "  actual:   $actual"
    return 1
  fi
}

assert_file_exists() {
  local desc="$1" file="$2"
  if [[ -f "$file" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — file not found: $file"
    return 1
  fi
}

assert_contains() {
  local desc="$1" needle="$2" haystack="$3"
  if [[ "$haystack" == *"$needle"* ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — '$needle' not found in output"
    return 1
  fi
}

# Demo usage
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'hello world' > "$TMPDIR/greeting.txt"
OUT=$(cat "$TMPDIR/greeting.txt")
assert_eq   'file content matches'     'hello world' "$OUT"
assert_file_exists 'greeting file created' "$TMPDIR/greeting.txt"
assert_contains    'output has hello'     'hello'       "$OUT"

Limiter la portée des variables d'environnement dans les essais

Les scripts lisent souvent des variables d'environnement comme $HOME, $CONFIG_PATH ou $DATABASE_URL. Lors des essais, vous devez les remplacer sans polluer l'environnement réel.

L'approche la plus sûre consiste à exécuter le script soumis à l'essai dans un sous-shell ne contenant que les variables que vous définissez explicitement. La commande env vous permet de supprimer l'environnement puis de rajouter uniquement ce dont vous avez besoin :

  • env -i VAR=val ./script.sh — environnement complètement vierge
  • (export VAR=val; ./script.sh) — le sous-shell hérite de l'environnement parent et y ajoute vos remplacements

Les sous-shells garantissent également que si le script modifie $IFS, $PWD ou un autre état global, ces modifications ne se propagent jamais à votre lanceur d'essais.

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Write a tiny script under test that reads env vars
cat > "$FIXTURE/deploy.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
ENV_NAME=${DEPLOY_ENV:-unknown}
CFG=${CONFIG_DIR:-/etc/app}
echo "Deploying to $ENV_NAME using config from $CFG"
SCRIPT
chmod +x "$FIXTURE/deploy.sh"

echo '--- Test 1: staging env ---'
# Run in a clean subshell with explicit vars
(
  export DEPLOY_ENV=staging
  export CONFIG_DIR="$FIXTURE/config"
  "$FIXTURE/deploy.sh"
)

echo '--- Test 2: production env ---'
(
  export DEPLOY_ENV=production
  export CONFIG_DIR=/etc/prod-config
  "$FIXTURE/deploy.sh"
)

echo '--- Test 3: defaults ---'
# No env vars set — script should use its own defaults
env -i PATH="$PATH" "$FIXTURE/deploy.sh"

Introduction à kcov pour la couverture Bash

La couverture répond à la question suivante : quelles lignes (et quelles branches) de mon script les essais ont-ils réellement exécutées ? Un taux de couverture élevé ne garantit pas l'exactitude, mais un taux faible révèle des chemins non testés susceptibles de contenir des bogues.

L'outil principal de couverture Bash est kcov. Il fonctionne en instrumentant le script au niveau du système d'exploitation avec PTRACE (Linux) ou dtrace (macOS), sans nécessiter de modification du code source. Il produit un rapport HTML affichant les lignes rouges (non couvertes) et vertes (couvertes).

Utilisation de base :

  • kcov --include-path=./src coverage-out/ ./src/myscript.sh
  • Ouvrez coverage-out/index.html dans un navigateur pour examiner les résultats
  • Dans l'intégration continue, analysez coverage-out/myscript.sh/coverage.json pour obtenir un pourcentage lisible par une machine

Remarque : kcov doit être installé séparément (brew install kcov sur macOS, apt install kcov sur Ubuntu 20.04 et versions ultérieures).

Exécuter kcov sur un script réel

Voici un exemple de bout en bout présentant un script déployable, un essai qui l'exécute et l'appel à kcov qui mesure la couverture. Remarquez que le répertoire de sortie est propre à chaque essai, afin de pouvoir fusionner ultérieurement les résultats de plusieurs exécutions.

#!/usr/bin/env bash
# This demo shows the *structure* of a kcov workflow.
# It will not run kcov itself (not guaranteed to be installed),
# but the script under test and test runner are fully runnable.
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# 1. Script under test
cat > "$FIXTURE/process.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
INPUT="$1"
if [[ ! -f "$INPUT" ]]; then
  echo "ERROR: file not found" >&2
  exit 1
fi
LINE_COUNT=$(wc -l < "$INPUT")
if (( LINE_COUNT == 0 )); then
  echo "WARNING: file is empty"
else
  echo "Processed $LINE_COUNT lines"
fi
SCRIPT
chmod +x "$FIXTURE/process.sh"

# 2. Happy path test (exercises line 6 and 11)
echo -e 'one\ntwo\nthree' > "$FIXTURE/data.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/data.txt")
echo "Happy path: $OUT"

# 3. Empty file test (exercises line 9)
touch "$FIXTURE/empty.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/empty.txt")
echo "Empty file: $OUT"

# 4. Missing file test (exercises line 5-6)
if ! OUT=$("$FIXTURE/process.sh" "$FIXTURE/missing.txt" 2>&1); then
  echo "Missing file (expected error): $OUT"
fi

# To measure coverage, wrap each call with kcov:
# kcov --include-path="$FIXTURE" "$FIXTURE/cov/happy" "$FIXTURE/process.sh" ...
# kcov --merge "$FIXTURE/cov/all" "$FIXTURE/cov/happy" "$FIXTURE/cov/empty"

Fusionner la couverture de plusieurs exécutions d'essais

Un seul essai couvre rarement toutes les branches. Vous exécutez plusieurs essais, chacun dans son propre répertoire de sortie de couverture, puis vous les fusionnez. L'option --merge de kcov combine plusieurs exécutions en un rapport unifié.

Le modèle habituel dans une chaîne de traitement d'intégration continue :

  • Exécuter l'essai A → sortie vers cov/test_a/
  • Exécuter l'essai B → sortie vers cov/test_b/
  • Fusionner → kcov --merge cov/all/ cov/test_a/ cov/test_b/
  • Analyser cov/all/<script>/coverage.json pour obtenir le pourcentage final

Vous pouvez également imposer un seuil minimal et faire échouer la compilation d'intégration continue si la couverture descend en dessous de ce seuil :

#!/usr/bin/env bash
# extract_coverage.sh — parse kcov JSON and fail below threshold
set -euo pipefail

MIN_COVERAGE=80   # percent
COV_JSON="${1:-coverage-out/myscript.sh/coverage.json}"

if [[ ! -f "$COV_JSON" ]]; then
  echo "ERROR: coverage JSON not found at $COV_JSON" >&2
  exit 1
fi

# kcov JSON contains a key like: "percent_covered": "87.50"
PERCENT=$(grep -oP '"percent_covered":\s*"\K[0-9.]+' "$COV_JSON")
PERCENT_INT=${PERCENT%%.*}   # truncate decimal

echo "Coverage: ${PERCENT}%  (minimum: ${MIN_COVERAGE}%)"

if (( PERCENT_INT < MIN_COVERAGE )); then
  echo "FAIL: coverage ${PERCENT}% is below threshold ${MIN_COVERAGE}%" >&2
  exit 1
fi

echo "PASS: coverage threshold met"

Couverture des branches ou couverture des lignes

Vous rencontrerez deux principales métriques de couverture :

  • Couverture des lignes — cette ligne a-t-elle été exécutée ? Cette métrique est facile à fausser : un seul essai peut parcourir de nombreuses lignes tout en ignorant d'importants chemins conditionnels.
  • Couverture des branches — chaque branche de chaque if, case et &&/|| a-t-elle été suivie ? Le signal est bien plus fiable. Il faut des essais pour le côté vrai et le côté faux de chaque décision.

kcov fournit les deux mesures. L'idée essentielle est la suivante : 100 % de couverture des lignes n'implique pas 100 % de couverture des branches. Considérez ce script : un seul essai avec un fichier non vide couvrira chaque ligne, mais la branche correspondant au fichier vide (ligne 9 ci-dessous) ne sera jamais atteinte :

#!/usr/bin/env bash
# Illustrates line vs branch coverage gap
set -euo pipefail

check_file() {
  local f="$1"
  if [[ -f "$f" ]]; then        # branch A (true) OR branch B (false)
    local lines
    lines=$(wc -l < "$f")
    if (( lines > 0 )); then    # branch C (true) OR branch D (false)
      echo "File has $lines lines"
    else
      echo "File is empty"     # branch D — unreached if only tested with non-empty file
    fi
  else
    echo "File missing"        # branch B — unreached if only tested with existing file
  fi
}

# Only one test: covers lines 5-10 (4 of 6 branches)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'data' > "$TMPDIR/sample.txt"
check_file "$TMPDIR/sample.txt"

# To reach 100% branch coverage you also need:
# check_file "/nonexistent/path"
# check_file "$TMPDIR/empty.txt" (after: touch "$TMPDIR/empty.txt")

Intégrer les fixtures et la couverture dans l’intégration continue

Pour tout regrouper, un pipeline d’intégration continue robuste pour les projets Bash combine la préparation des fixtures, l’exécution des tests avec kcov, la fusion et la vérification d’un seuil dans un seul script. Ce script devient le point d’entrée de l’intégration continue : une seule commande suffit pour tout exécuter.

Principes de conception de l’exécuteur de tests d’intégration continue :

  • Chaque cas de test appelle setup_fixture et enregistre teardown_fixture via trap
  • Le script testé est appelé avec un $PATH fictif et des variables d’environnement limitées à sa portée
  • kcov enveloppe chaque appel et écrit les résultats dans un sous-répertoire numéroté
  • Une fois tous les tests terminés, kcov fusionne les résultats et le script de seuil bloque ou autorise la compilation
  • L’exécuteur d’intégration continue se termine avec un code différent de zéro si un test ou la vérification de couverture échoue
#!/usr/bin/env bash
# ci_runner.sh — full fixture + coverage pipeline entry point
set -euo pipefail

SRC="./src/deploy.sh"
COV_ROOT="$(mktemp -d)/coverage"
trap 'rm -rf "$COV_ROOT"' EXIT
mkdir -p "$COV_ROOT"

PASS=0
FAIL=0
RUN_NUM=0

run_test() {
  local name="$1" test_fn="$2"
  local fixture
  fixture=$(mktemp -d)
  local cov_out="$COV_ROOT/run_$((++RUN_NUM))"

  if (
    trap 'rm -rf "$fixture"' EXIT
    export FIXTURE="$fixture"
    # Fake bin directory shadowing real tools
    mkdir -p "$fixture/bin"
    export PATH="$fixture/bin:$PATH"
    "$test_fn" "$fixture"
  ); then
    echo "PASS: $name"
    (( PASS++ )) || true
  else
    echo "FAIL: $name"
    (( FAIL++ )) || true
  fi
}

# Example test function
test_happy_path() {
  local fx="$1"
  mkdir -p "$fx/dist"
  echo 'app.js' > "$fx/dist/index.js"
  # Would normally run: kcov "$cov_out" "$SRC" --env=staging "$fx"
  echo "[test] happy path executed in $fx"
}

run_test 'happy_path' test_happy_path

echo "Results: $PASS passed, $FAIL failed"
(( FAIL == 0 ))

Vérification des connaissances : technique du PATH fictif

Vérifiez votre compréhension de la technique du PATH fictif utilisée dans les fixtures de test Bash.

Récapitulatif : fixtures, environnements temporaires et couverture

Cette leçon a présenté l’ensemble des outils nécessaires pour réaliser des tests Bash fiables et isolés :

  • Répertoires temporaires — mktemp -d associé à un trap ... EXIT garantit un nettoyage automatique, quelle que soit la façon dont le test se termine.
  • Structure des fixtures — un duo setup_fixture / teardown_fixture crée puis supprime une arborescence minimale qui reproduit les entrées réelles des scripts.
  • PATH fictif — placez des exécutables simulés dans $FIXTURE/bin/ et ajoutez ce répertoire au début de $PATH pour intercepter les appels à curl, aws, docker ou tout autre outil externe sans toucher aux exécutables du système.
  • Portée de l’environnement — exécutez le script testé dans un sous-shell (() ou env -i) afin que les variables modifiées ne se propagent jamais à l’exécuteur de tests.
  • Assertions — de petites fonctions utilitaires (assert_eq, assert_file_exists, assert_contains) produisent une sortie claire indiquant la réussite ou l’échec, ainsi que des messages d’erreur pertinents.
  • Couverture avec kcov — kcov enveloppe l’exécution des scripts sans modifier leur code source et produit des rapports HTML et JSON de couverture des lignes et des branches.
  • Fusion et seuil — combinez plusieurs exécutions de kcov avec --merge, analysez le JSON et faites échouer l’intégration continue si la couverture descend sous votre minimum.
  • Couverture des branches et des lignes — ciblez toujours la couverture des branches ; la couverture des lignes seule peut laisser passer des chemins conditionnels entiers et donner une fausse impression de confiance.

Grâce à ces techniques, vos tests Bash deviennent aussi rigoureux que ceux de n’importe quel langage compilé.

Questions Fréquemment Posées

La leçon « Dispositifs de test, environnements temporaires et couverture » est-elle gratuite ?

Oui — le texte complet de « Dispositifs de test, environnements temporaires et couverture » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DevOps Bootcamp, passe à CoddyKit PRO. Le cours DevOps Bootcamp comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Dispositifs de test, environnements temporaires et couverture » ?

Créez des dispositifs de test isolés et mesurez les branches de vos scripts réellement parcourues par vos tests. Tu pratiques DevOps Bootcamp avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer DevOps Bootcamp ?

Aucune expérience préalable n'est requise. DevOps Bootcamp sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.

Combien de temps prend la leçon « Dispositifs de test, environnements temporaires et couverture » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon DevOps Bootcamp ?

Oui. Chaque leçon DevOps Bootcamp inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Tester les fonctions avec Bats-core
  2. Simuler des commandes et créer des doublures d’outils externes
  3. Dispositifs de test, environnements temporaires et couverture
  4. Exécuter des tests Shell dans les pipelines CI
← Retour à DevOps Bootcamp