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()oucleanup()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_fixtureSimuler 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 :
- Créez un répertoire temporaire
bin/à l'intérieur de votre dispositif - Écrivez-y de petits scripts shell portant les mêmes noms que les vrais outils
- Ajoutez ce répertoire au début de
$PATHavant 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.htmldans un navigateur pour examiner les résultats - Dans l'intégration continue, analysez
coverage-out/myscript.sh/coverage.jsonpour 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.jsonpour 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,caseet&&/||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_fixtureet enregistreteardown_fixtureviatrap - Le script testé est appelé avec un
$PATHfictif 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 -dassocié à untrap ... EXITgarantit un nettoyage automatique, quelle que soit la façon dont le test se termine. - Structure des fixtures — un duo
setup_fixture/teardown_fixturecré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$PATHpour intercepter les appels àcurl,aws,dockerou 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 (
()ouenv -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
- Tester les fonctions avec Bats-core
- Simuler des commandes et créer des doublures d’outils externes
- Dispositifs de test, environnements temporaires et couverture
- Exécuter des tests Shell dans les pipelines CI