Linux-kommandolinjen og Bash-scripting på ekspertniveau · Lektion

Test-fixtures, midlertidige miljøer og dækning

Opbyg isolerede test-fixtures, og mål hvilke grene i scriptet testene faktisk gennemløber.

Lektion 3 af 413 trin

Test-fixtures, midlertidige miljøer og dækning er en gratis Linux-kommandolinjen og Bash-scripting på ekspertniveau-lektion på CoddyKit. Dette er lektion 3 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Linux-kommandolinjen og Bash-scripting på ekspertniveau, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Linux-kommandolinjen og Bash-scripting på ekspertniveau-kurset indeholder 4 lektioner i alt.

Hvorfor test-fixtures og isolation er vigtige

Når du tester Bash-scripts, er den største risiko bivirkninger: at dine tests ved et uheld ændrer rigtige filer, rigtige databaser eller den virkelige systemtilstand. En test, der består på din maskine, men ødelægger produktionsdata, er værre end slet ingen test.

Løsningen er test-fixtures — kontrollerede, midlertidige miljøer, der afspejler virkelige forhold uden at røre noget virkeligt. Gode fixtures giver dig:

  • Reproducerbarhed — tests giver det samme resultat hver gang
  • Isolation — tests påvirker ikke hinanden eller værtsmaskinen
  • Sikkerhed — destruktive handlinger berører kun data, der kan kasseres
  • Hastighed — ingen netværkskald og ingen tung I/O, medmindre det er absolut nødvendigt

I Bash-testning er fixtures typisk midlertidige mapper med kendte filer, falske eksekverbare filer placeret tidligt i $PATH og miljøvariabler, der er begrænset til testprocessen.

Oprettelse og oprydning af midlertidige mapper

Det almindelige mønster for en midlertidig mappe pr. test bruger mktemp -d, som opretter en unik mappe i /tmp og udskriver dens sti. Du gemmer stien og registrerer en trap, der automatisk fjerner mappen, når shellen afsluttes — også ved fejl.

Dette idiom på to linjer bør findes i enhver testfil, der arbejder med filsystemet:

#!/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'

Strukturering af et fixture-mappetræ

En velstruktureret fixture afspejler den mappestruktur, som dit script under test faktisk forventer. Tænk på den som en miniature-rodmappe for et falsk projekt. Fixture-opsætningsfunktionen opretter denne struktur før hver test, og oprydningen fjerner den.

Vigtige fremgangsmåder:

  • Brug en setup()-funktion, som dit testframework kalder før hver test
  • Brug en teardown() eller cleanup(), der kører efter hver test, også ved fejl
  • Hold fixture-filer minimale — kun det, som scriptet faktisk læser
  • Navngiv fixture-filer beskrivende, så fejl er nemme at diagnosticere
#!/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

Mocking af eksekverbare filer med en falsk PATH

Mange Bash-scripts kalder eksterne værktøjer som curl, aws, docker eller git. I tests vil du ikke ramme rigtige tjenester, så du erstatter disse værktøjer med falske eksekverbare filer.

Teknikken er enkel:

  1. Opret en midlertidig bin/-mappe i din fixture
  2. Skriv små shell-scripts dér med samme navne som de rigtige værktøjer
  3. Sæt mappen forrest i $PATH, før du kalder dit script under test

Da $PATH gennemsøges fra venstre mod højre, vinder den falske fil. Den rigtige binærfil bliver aldrig kaldt.

#!/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"

Opsamling og kontrol af kommandouddata

En fixture er kun nyttig, hvis du kan kontrollere, hvad dit script gjorde. De almindelige mønstre er:

  • Opsaml stdout/stderr i variabler med $() eller proces-substitution
  • Undersøg logfiler, som falske binærfiler har skrevet
  • Kontrollér afslutningskoder eksplicit med $? eller betinget logik
  • Kontrollér, at bestemte filer blev oprettet, ændret eller ikke blev berørt

Hvis du skriver små, fokuserede hjælpefunktioner til kontrol, bliver dine tests læsbare, og du får præcise fejlmeddelelser, når noget går galt.

#!/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"

Begrænsning af miljøvariablers synlighed i tests

Scripts læser ofte fra miljøvariabler som $HOME, $CONFIG_PATH eller $DATABASE_URL. I tests skal du overskrive disse uden at forurene det rigtige miljø.

Den sikreste metode er at køre scriptet under test i en subshell med kun de variabler, du eksplicit angiver. Kommandoen env lader dig reducere miljøet og derefter tilføje netop det, du har brug for:

  • env -i VAR=val ./script.sh — helt rent miljø
  • (export VAR=val; ./script.sh) — subshell nedarver det overordnede miljø samt dine overskrivninger

Brug af subshells betyder også, at hvis scriptet ændrer $IFS, $PWD eller anden global tilstand, slipper ændringerne aldrig tilbage til din testrunner.

#!/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"

Introduktion til kcov til Bash-dækning

Dækning besvarer spørgsmålet: Hvilke linjer (og grene) i mit script udførte testene faktisk? Et højt dæknings-tal garanterer ikke korrekthed, men et lavt tal afslører uprøvede stier, som sandsynligvis indeholder fejl.

Det vigtigste værktøj til Bash-dækning er kcov. Det fungerer ved at instrumentere scriptet på OS-niveau med PTRACE (Linux) eller dtrace (macOS), så det ikke kræver ændringer i kildekoden. Det producerer en HTML-rapport, der viser røde (ikke dækkede) og grønne (dækkede) linjer.

Grundlæggende brug:

  • kcov --include-path=./src coverage-out/ ./src/myscript.sh
  • Åbn coverage-out/index.html i en browser for at undersøge resultaterne
  • I CI skal du fortolke coverage-out/myscript.sh/coverage.json for at få en maskinlæsbar procentværdi

Bemærk: kcov skal installeres separat (brew install kcov på macOS, apt install kcov på Ubuntu 20.04+).

Kørsel af kcov mod et rigtigt script

Her er et eksempel fra start til slut, der viser et script, som kan sættes i drift, en test, der gennemgår det, og kcov-kaldet, der måler dækningen. Bemærk, at outputmappen er specifik for testen, så du senere kan sammenflette resultater fra flere testkørsler.

#!/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"

Sammenfletning af dækning fra flere testkørsler

En enkelt test dækker sjældent alle grene. Du kører flere tests — hver i sin egen outputmappe for dækning — og sammenfletter dem derefter. kcow's --merge-flag kombinerer flere kørsler til én samlet rapport.

Det typiske mønster i en CI-pipeline:

  • Kør test A → output til cov/test_a/
  • Kør test B → output til cov/test_b/
  • Sammenflet → kcov --merge cov/all/ cov/test_a/ cov/test_b/
  • Fortolk cov/all/<script>/coverage.json for at få den endelige procentværdi

Du kan også håndhæve en minimumsgrænse og få CI-buildet til at mislykkes, hvis dækningen falder under den:

#!/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"

Gren-dækning sammenlignet med linjedækning

Der er to primære dækningsmålinger, som du vil møde:

  • Linjedækning — blev denne linje overhovedet udført? Den er nem at manipulere med: En enkelt test kan berøre mange linjer, samtidig med at den overser vigtige betingede stier.
  • Gren-dækning — blev hver gren i hver if, case og &&/|| valgt? Det er et langt stærkere signal. Det kræver tests af både den sande og den falske side af hver beslutning.

kcov rapporterer begge dele. Den vigtigste pointe er: 100 % linjedækning medfører ikke 100 % gren-dækning. Overvej dette script — en enkelt test med en ikke-tom fil dækker hver linje, men grenen for en tom fil (linje 9 nedenfor) nås aldrig:

#!/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")

Integration af testfixtures og kodedækning i CI

Her samles det hele: Et robust CI-forløb til Bash-projekter kombinerer opsætning af testfixtures, testkørsel under kcov, sammenfletning og en tærskelkontrol i ét script. Dette script bliver indgangspunktet til CI — én kommando til at køre det hele.

Designprincipper for CI-testkøreren:

  • Hver test case kalder setup_fixture og registrerer teardown_fixture via trap
  • Scripten, der testes, kaldes med en falsk $PATH og afgrænsede miljøvariabler
  • kcov omslutter hver kørsel og skriver til en nummereret undermappe
  • Efter alle tests sammenfletter kcov resultaterne, og tærskelscripten fungerer som kontrol for bygningen
  • CI-testkøreren afsluttes med en fejlkode, hvis en test eller kodedækningskontrollen mislykkes
#!/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 ))

Videnscheck: Teknikken med falsk PATH

Test din forståelse af teknikken med falsk PATH, som bruges i Bash-testfixtures.

Opsamling: Testfixtures, midlertidige miljøer og kodedækning

Denne lektion gennemgik hele værktøjskassen til pålidelig og isoleret Bash-testning:

  • Midlertidige mapper — mktemp -d sammen med en trap ... EXIT garanterer automatisk oprydning, uanset hvordan testen afsluttes.
  • Testfixturestruktur — Et par bestående af setup_fixture og teardown_fixture udfylder og sletter et minimalt mappetræ, der afspejler virkelige input til scripts.
  • Falsk PATH — Placér stub-kørbare filer i $FIXTURE/bin/, og indsæt den forrest i $PATH for at opfange kald til curl, aws, docker eller et hvilket som helst eksternt værktøj uden at røre systemets binærfiler.
  • Afgrænsning af miljøet — Kør scripten, der testes, i en subshell (() eller env -i), så ændrede variabler aldrig lækker tilbage til testkøreren.
  • Påstande — Små hjælpefunktioner (assert_eq, assert_file_exists, assert_contains) giver tydeligt bestået/mislykket-output og meningsfulde fejlmeddelelser.
  • kcov-kodedækning — Omslutter scriptafkørsel uden ændringer af kildekoden og producerer HTML- og JSON-rapporter for linje- og grendækning.
  • Sammenfletning og tærskel — Kombinér flere kcov-kørsler med --merge, fortolk JSON-resultatet, og lad CI mislykkes, hvis kodedækningen falder under dit minimum.
  • Grendækning kontra linjedækning — Sigt altid efter grendækning; linjedækning alene kan overse hele betingede forløb og give falsk tryghed.

Med disse teknikker bliver dine Bash-tests lige så grundige som tests af ethvert kompileret sprog.

Gratis at komme i gang

Lær Bash med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
22
Lektioner
88

Ofte stillede spørgsmål

Er lektionen “Test-fixtures, midlertidige miljøer og dækning” gratis?

Ja — hele teksten til “Test-fixtures, midlertidige miljøer og dækning” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Linux-kommandolinjen og Bash-scripting på ekspertniveau-kurset, skal du opgradere til CoddyKit PRO. Linux-kommandolinjen og Bash-scripting på ekspertniveau-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Test-fixtures, midlertidige miljøer og dækning”?

Opbyg isolerede test-fixtures, og mål hvilke grene i scriptet testene faktisk gennemløber. Du øver dig i Linux-kommandolinjen og Bash-scripting på ekspertniveau med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Linux-kommandolinjen og Bash-scripting på ekspertniveau?

Der kræves ingen tidligere erfaring. Linux-kommandolinjen og Bash-scripting på ekspertniveau på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 3 af 4.

Hvor lang tid tager lektionen “Test-fixtures, midlertidige miljøer og dækning”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Linux-kommandolinjen og Bash-scripting på ekspertniveau-lektion?

Ja. Alle Linux-kommandolinjen og Bash-scripting på ekspertniveau-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Enhedstest af funktioner med Bats-core
  2. Mocking af kommandoer og stubbing af eksterne værktøjer
  3. Test-fixtures, midlertidige miljøer og dækning
  4. Kørsel af shelltests i CI-pipelines
← Tilbage til Linux-kommandolinjen og Bash-scripting på ekspertniveau