Fixture, ambienti temporanei e coverage
Crei fixture di test isolate e misuri quali rami degli script vengono effettivamente eseguiti dai test.
Fixture, ambienti temporanei e coverage è una lezione Linux Command Line & Bash Scripting Mastery gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Linux Command Line & Bash Scripting Mastery, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Linux Command Line & Bash Scripting Mastery include 4 lezioni in totale.
Perché i fixture e l'isolamento sono importanti
Quando si testano script Bash, il rischio maggiore è rappresentato dagli effetti collaterali: i test potrebbero modificare accidentalmente file, database o lo stato del sistema reali. Un test che supera la verifica sul proprio computer ma danneggia i dati di produzione è peggiore dell'assenza di test.
La soluzione consiste nei fixture di test: ambienti controllati e temporanei che rispecchiano le condizioni reali senza modificare nulla di reale. I fixture efficaci offrono:
- Riproducibilità — i test producono lo stesso risultato a ogni esecuzione
- Isolamento — i test non interferiscono tra loro né con il sistema host
- Sicurezza — le operazioni distruttive interessano solo dati usa e getta
- Velocità — nessuna chiamata di rete e nessun I/O pesante, salvo assoluta necessità
Nei test Bash, i fixture sono in genere directory temporanee popolate con file noti, eseguibili simulati collocati all'inizio di $PATH e variabili d'ambiente limitate al processo del test.
Creare e pulire le directory temporanee
Il modello standard per una directory temporanea dedicata a ogni test utilizza mktemp -d, che crea una directory univoca in /tmp e ne stampa il percorso. Si memorizza il percorso e si registra un trap per rimuoverla automaticamente quando la shell termina, anche in caso di errore.
Questa espressione idiomatica di due righe dovrebbe comparire in ogni file di test che accede al filesystem:
#!/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'Strutturare l'albero delle directory dei fixture
Un fixture ben strutturato rispecchia la struttura delle directory che lo script sottoposto al test si aspetta effettivamente. Lo si può considerare una radice di progetto fittizia in miniatura. La funzione di configurazione del fixture crea questa struttura prima di ogni test e la funzione di pulizia la rimuove.
Pratiche fondamentali:
- Utilizzare una funzione
setup()che il framework di test chiama prima di ogni test - Utilizzare una funzione
teardown()ocleanup()eseguita dopo ogni test, anche in caso di errore - Mantenere i file dei fixture essenziali: solo quelli che lo script legge effettivamente
- Dare ai file dei fixture nomi descrittivi, così da diagnosticare facilmente gli errori
#!/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_fixtureSimulare gli eseguibili con un PATH fittizio
Molti script Bash chiamano strumenti esterni come curl, aws, docker o git. Nei test non si desidera accedere ai servizi reali, quindi si sostituiscono questi strumenti con eseguibili fittizi.
La tecnica è semplice:
- Creare una directory temporanea
bin/all'interno del fixture - Scrivervi piccoli script shell con gli stessi nomi degli strumenti reali
- Anteporre questa directory a
$PATHprima di chiamare lo script sottoposto al test
Poiché $PATH viene cercato da sinistra a destra, viene scelto l'eseguibile fittizio. Il binario reale non viene mai invocato.
#!/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"Acquisire e verificare l'output dei comandi
Un fixture è utile solo se è possibile verificare ciò che ha fatto lo script. I modelli standard sono:
- Acquisire stdout/stderr in variabili con
$()o la sostituzione di processo - Esaminare i file di log scritti dagli eseguibili fittizi
- Controllare esplicitamente i codici di uscita con
$?o la logica condizionale - Verificare che determinati file siano stati creati, modificati o lasciati intatti
Scrivere piccoli helper di verifica mirati rende i test leggibili e fornisce messaggi di errore precisi quando qualcosa non funziona.
#!/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"Gestire l'ambito delle variabili d'ambiente nei test
Gli script leggono spesso variabili d'ambiente come $HOME, $CONFIG_PATH o $DATABASE_URL. Nei test è necessario sovrascriverle senza contaminare l'ambiente reale.
L'approccio più sicuro consiste nell'eseguire lo script sottoposto al test in una subshell con solo le variabili impostate esplicitamente. Il comando env consente di ridurre l'ambiente e aggiungere nuovamente solo ciò che serve:
env -i VAR=val ./script.sh— ambiente completamente pulito(export VAR=val; ./script.sh)— la subshell eredita l'ambiente del processo padre oltre alle sostituzioni effettuate
L'utilizzo delle subshell significa inoltre che, se lo script modifica $IFS, $PWD o un altro stato globale, tali modifiche non si propagano mai al test runner.
#!/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"Introduzione a kcov per la coverage Bash
La coverage risponde alla domanda: quali righe (e quali rami) del mio script sono state effettivamente eseguite dai test? Un'elevata percentuale di coverage non garantisce la correttezza, ma una percentuale bassa rivela percorsi non testati che potrebbero contenere bug.
Lo strumento principale per la coverage Bash è kcov. Funziona strumentando lo script a livello del sistema operativo tramite PTRACE (Linux) o dtrace (macOS), quindi non richiede modifiche al codice sorgente. Produce un report HTML che mostra in rosso le righe non coperte e in verde quelle coperte.
Utilizzo di base:
kcov --include-path=./src coverage-out/ ./src/myscript.sh- Aprire
coverage-out/index.htmlin un browser per esaminare i risultati - In CI, analizzare
coverage-out/myscript.sh/coverage.jsonper ottenere una percentuale leggibile dalle macchine
Nota: kcov deve essere installato separatamente (brew install kcov su macOS, apt install kcov su Ubuntu 20.04+).
Eseguire kcov su uno script reale
Ecco un esempio completo che mostra uno script distribuibile, un test che lo esercita e l'invocazione di kcov che misura la coverage. Noti che la directory di output è specifica per ogni test, così sarà possibile unire in seguito i risultati di più esecuzioni.
#!/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"Unire la coverage di più esecuzioni dei test
Un singolo test raramente copre tutti i rami. Si eseguono più test, ciascuno nella propria directory di output della coverage, e poi li si unisce. Il flag --merge di kcov combina più esecuzioni in un unico report.
Il modello tipico in una pipeline CI:
- Eseguire il test A → output in
cov/test_a/ - Eseguire il test B → output in
cov/test_b/ - Unire →
kcov --merge cov/all/ cov/test_a/ cov/test_b/ - Analizzare
cov/all/<script>/coverage.jsonper ottenere la percentuale finale
È inoltre possibile imporre una soglia minima e interrompere la build CI se la coverage scende al di sotto di tale soglia:
#!/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"Coverage dei rami e coverage delle righe a confronto
Esistono due metriche principali di coverage che incontrerà:
- Coverage delle righe — questa riga è stata eseguita? È facile da aggirare: un singolo test può toccare molte righe senza coprire importanti percorsi condizionali.
- Coverage dei rami — è stato seguito ogni ramo di ogni
if,casee&&/||? È un indicatore molto più significativo. Richiede test sia per il ramo vero sia per quello falso di ogni decisione.
kcov restituisce entrambe le metriche. L'idea fondamentale è che la coverage delle righe al 100% non implica una coverage dei rami al 100%. Consideri questo script: un singolo test con un file non vuoto coprirà ogni riga, ma il ramo relativo al file vuoto (la riga 9 qui sotto) non verrà mai raggiunto:
#!/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")Integrazione di fixture e coverage nella CI
Riunendo tutti gli elementi: una pipeline CI solida per i progetti Bash combina la configurazione delle fixture, l'esecuzione dei test con kcov, l'unione dei risultati e il controllo della soglia in un unico script. Questo script diventa il punto di ingresso della CI: un solo comando per eseguire tutto.
Principi di progettazione del test runner della CI:
- Ogni caso di test chiama
setup_fixturee registrateardown_fixturetramitetrap - Lo script sottoposto a test viene invocato con un
$PATHfittizio e variabili d'ambiente con ambito limitato - kcov avvolge ogni invocazione e scrive i risultati in una sottodirectory numerata
- Dopo tutti i test, kcov unisce i risultati e lo script della soglia determina se la build può procedere
- Il runner della CI termina con un codice diverso da zero se un test o il controllo della coverage fallisce
#!/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 ))Verifica delle conoscenze: tecnica del PATH fittizio
Verifichi la Sua comprensione della tecnica del PATH fittizio utilizzata nelle fixture dei test Bash.
Riepilogo: fixture, ambienti temporanei e coverage
In questa lezione ha esaminato l'intero insieme di strumenti per eseguire test Bash affidabili e isolati:
- Directory temporanee —
mktemp -dinsieme atrap ... EXITgarantisce la pulizia automatica, indipendentemente da come termina il test. - Struttura delle fixture — una coppia
setup_fixture/teardown_fixturecrea e distrugge una struttura di directory minima che rispecchia gli input reali degli script. - PATH fittizio — inserisca gli eseguibili stub in
$FIXTURE/bin/e lo anteponga a$PATHper intercettare le chiamate acurl,aws,dockero qualsiasi strumento esterno senza modificare i binari di sistema. - Ambito delle variabili d'ambiente — esegua lo script sottoposto a test in una subshell (
()oenv -i), così le variabili modificate non vengono mai propagate al test runner. - Asserzioni — piccole funzioni di supporto (
assert_eq,assert_file_exists,assert_contains) producono un output chiaro di successo o fallimento e messaggi di errore significativi. - Coverage con kcov — avvolge l'esecuzione degli script senza richiedere modifiche al codice sorgente e produce report HTML e JSON per la coverage di righe e branch.
- Unione e soglia — combini più esecuzioni di kcov con
--merge, analizzi il JSON e faccia fallire la CI se la coverage scende sotto il minimo stabilito. - Coverage dei branch e delle righe — punti sempre alla coverage dei branch; la sola coverage delle righe può non rilevare interi percorsi condizionali e dare una falsa sensazione di sicurezza.
Con queste tecniche, i Suoi test Bash diventano rigorosi quanto i test di qualsiasi linguaggio compilato.
Domande Frequenti
La lezione «Fixture, ambienti temporanei e coverage» è gratuita?
Sì — il testo completo di «Fixture, ambienti temporanei e coverage» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Linux Command Line & Bash Scripting Mastery, passa a CoddyKit PRO. Il corso Linux Command Line & Bash Scripting Mastery include 4 lezioni in totale.
Cosa imparerò in «Fixture, ambienti temporanei e coverage»?
Crei fixture di test isolate e misuri quali rami degli script vengono effettivamente eseguiti dai test. Eserciti Linux Command Line & Bash Scripting Mastery con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Linux Command Line & Bash Scripting Mastery?
Non è richiesta alcuna esperienza precedente. Linux Command Line & Bash Scripting Mastery su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.
Quanto tempo richiede la lezione «Fixture, ambienti temporanei e coverage»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Linux Command Line & Bash Scripting Mastery?
Sì. Ogni lezione Linux Command Line & Bash Scripting Mastery include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Test unitari delle funzioni con Bats-core
- Simulare comandi e creare stub per strumenti esterni
- Fixture, ambienti temporanei e coverage
- Eseguire test shell nelle pipeline CI