Mestring av Linux-kommandolinjen og Bash-skripting · leksjon

Testfiksturer, midlertidige miljøer og dekningsgrad

Bygg isolerte testfiksturer og mål hvilke grener i skriptene testene faktisk dekker.

Leksjon 3 av 413 trinn

Testfiksturer, midlertidige miljøer og dekningsgrad er en gratis leksjon i Mestring av Linux-kommandolinjen og Bash-skripting på CoddyKit. Dette er leksjon 3 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Mestring av Linux-kommandolinjen og Bash-skripting, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Mestring av Linux-kommandolinjen og Bash-skripting inneholder totalt 4 leksjoner.

Hvorfor test-fixtures og isolasjon er viktige

Ved testing av Bash-skript er den største risikoen bivirkninger: Testene endrer ved et uhell ekte filer, ekte databaser eller den faktiske systemtilstanden. En test som består på maskinen Deres, men ødelegger produksjonsdata, er verre enn ingen test i det hele tatt.

Løsningen er test-fixtures – kontrollerte, kortvarige miljøer som gjenspeiler virkelige forhold uten å berøre noe ekte. Gode fixtures gir Dem:

  • Reproduserbarhet – testene gir samme resultat hver gang
  • Isolasjon – testene forstyrrer ikke hverandre eller vertsmaskinen
  • Sikkerhet – destruktive operasjoner berører bare data som kan kastes
  • Hastighet – ingen nettverkskall og ingen omfattende I/O med mindre det er helt nødvendig

Ved Bash-testing er fixtures vanligvis midlertidige kataloger fylt med kjente filer, falske kjørbare filer som plasseres tidlig i $PATH, og miljøvariabler som er begrenset til testprosessen.

Opprette og rydde opp i midlertidige kataloger

Det standardiserte mønsteret for en midlertidig katalog per test bruker mktemp -d, som oppretter en unik katalog i /tmp og skriver ut banen til den. De lagrer banen og registrerer en trap for å fjerne katalogen automatisk når skallet avsluttes – også ved feil.

Dette tolinjers idiomet bør finnes i alle testfiler som berører 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'

Strukturere et katalogtre for fixtures

En godt strukturert fixture gjenspeiler katalogoppsettet som skriptet som testes, faktisk forventer. Tenk på den som en miniatyrisert falsk prosjektrot. Fixture-oppsettfunksjonen oppretter dette oppsettet før hver test, og teardown fjerner det.

Viktig praksis:

  • Bruk en setup()-funksjon som testrammeverket kaller før hver test
  • Bruk en teardown() eller cleanup() som kjøres etter hver test, også ved feil
  • Hold fixture-filene minimale – bare det skriptet faktisk leser
  • Gi fixture-filene beskrivende navn, slik at feil blir enkle å diagnostisere
#!/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

Mocke kjørbare filer med en falsk PATH

Mange Bash-skript kaller eksterne verktøy som curl, aws, docker eller git. I tester ønsker De ikke å kontakte ekte tjenester, så De erstatter verktøyene med falske kjørbare filer.

Teknikken er enkel:

  1. Opprett en midlertidig bin/-katalog i fixturen
  2. Skriv små shell-skript der med samme navn som de ekte verktøyene
  3. Legg katalogen først i $PATH før De kaller skriptet som testes

Ettersom $PATH søkes gjennom fra venstre mot høyre, vinner den falske filen. Den ekte binærfilen blir aldri kalt.

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

Fange opp og kontrollere kommandoutdata

En fixture er bare nyttig hvis De kan kontrollere hva skriptet gjorde. Standardmønstrene er:

  • Fang stdout/stderr i variabler med $() eller prosessubstitusjon
  • Undersøk loggfiler som er skrevet av falske binærfiler
  • Kontroller avslutningskoder eksplisitt med $? eller betinget logikk
  • Bekreft at bestemte filer ble opprettet, endret eller ikke berørt

Hvis De skriver små, fokuserte hjelpefunksjoner for kontroll, blir testene lettere å lese, og De får presise feilmeldinger når noe 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"

Omfang for miljøvariabler i tester

Skript leser ofte fra miljøvariabler som $HOME, $CONFIG_PATH eller $DATABASE_URL. I tester må De overstyre disse uten å forurense det virkelige miljøet.

Den tryggeste tilnærmingen er å kjøre skriptet som testes, i et underskall med bare variablene De angir eksplisitt. Kommandoen env lar Dem redusere miljøet og legge tilbake bare det De trenger:

  • env -i VAR=val ./script.sh – helt rent miljø
  • (export VAR=val; ./script.sh) – underskallet arver det overordnede miljøet og får i tillegg overstyringene Deres

Bruk av underskall innebærer også at hvis skriptet endrer $IFS, $PWD eller annen global tilstand, slipper disse endringene aldri tilbake til testkjøringen.

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

Introduksjon til kcov for Bash-dekning

Dekningsgrad svarer på spørsmålet: Hvilke linjer (og grener) i skriptet ble faktisk kjørt av testene? En høy dekningsgrad garanterer ikke at koden er riktig, men en lav dekningsgrad avdekker uprøvde kjørebaner som sannsynligvis kan inneholde feil.

Hovedverktøyet for Bash-dekning er kcov. Det fungerer ved å instrumentere skriptet på operativsystemnivå ved hjelp av PTRACE (Linux) eller dtrace (macOS), og krever derfor ingen endringer i kildekoden. Det produserer en HTML-rapport som viser røde linjer (ikke dekket) og grønne linjer (dekket).

Grunnleggende bruk:

  • kcov --include-path=./src coverage-out/ ./src/myscript.sh
  • Åpne coverage-out/index.html i en nettleser for å undersøke resultatene
  • Parse coverage-out/myscript.sh/coverage.json i CI for å få en maskinlesbar prosentverdi

Merk: kcov må installeres separat (brew install kcov på macOS, apt install kcov på Ubuntu 20.04+).

Kjøre kcov mot et ekte skript

Her er et komplett eksempel som viser et distribuerbart skript, en test som kjører det, og kcov-kallet som måler dekningen. Legg merke til at utdatakatalogen er separat for hver test, slik at De kan slå sammen resultater fra flere testkjøringer senere.

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

Slå sammen dekning fra flere testkjøringer

En enkelt test dekker sjelden alle grener. De kjører flere tester – hver i sin egen katalog for dekningsutdata – og slår dem deretter sammen. kcovs --merge-flagg kombinerer flere kjøringer til én samlet rapport.

Det typiske mønsteret i en CI-pipeline:

  • Kjør test A → skriv utdata til cov/test_a/
  • Kjør test B → skriv utdata til cov/test_b/
  • Slå sammen → kcov --merge cov/all/ cov/test_a/ cov/test_b/
  • Parse cov/all/<script>/coverage.json for å hente den endelige prosentverdien

De kan også håndheve en minimumsgrense og la CI-byggingen mislykkes hvis dekningen faller 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-dekning kontra linjedekning

Det finnes to hovedmål for dekningsgrad som De vil møte:

  • Linjedekning – ble denne linjen kjørt i det hele tatt? Den er enkel å manipulere: Én enkelt test kan berøre mange linjer uten å dekke viktige betingede kjørebaner.
  • Gren-dekning – ble hver gren i hver if, case og &&/|| fulgt? Dette gir et langt sterkere signal. Det krever tester for både den sanne og den usanne siden av hver avgjørelse.

kcov rapporterer begge deler. Den viktige innsikten er: 100 % linjedekning innebærer ikke 100 % gren-dekning. Se på dette skriptet – én enkelt test med en fil som ikke er tom, vil dekke hver linje, men grenen for tom fil (linje 9 nedenfor) blir aldri nådd:

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

Integrering av fixtures og dekning i CI

La oss sette alt sammen: En robust CI-pipeline for Bash-prosjekter kombinerer oppsett av fixtures, testkjøring med kcov, sammenslåing og en terskelsjekk i ett enkelt skript. Dette skriptet blir inngangspunktet for CI – én kommando for å kjøre alt.

Designprinsipper for CI-testkjøreren:

  • Hver test case kaller setup_fixture og registrerer teardown_fixture via trap
  • Skriptet som testes, startes med en falsk $PATH og avgrensede miljøvariabler
  • kcov pakker inn hver kjøring og skriver resultatet til en nummerert undermappe
  • Etter alle testene slår kcov sammen resultatene, og terskelskriptet avgjør om byggingen får passere
  • CI-kjøreren avsluttes med en status ulik null hvis en test eller dekning-sjekken feiler
#!/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 ))

Kunnskapssjekk: Teknikk med falsk PATH

Test forståelsen Deres av teknikken med falsk PATH som brukes i Bash-test-fixtures.

Oppsummering: Fixtures, midlertidige miljøer og dekning

Denne leksjonen dekket hele verktøykassen for pålitelig og isolert testing av Bash:

  • Midlertidige mapper – mktemp -d sammen med en trap ... EXIT garanterer automatisk opprydding, uansett hvordan testen avsluttes.
  • Fixture-struktur – Et par med setup_fixture og teardown_fixture oppretter og sletter et minimalt mappetre som gjenspeiler virkelige inndata til skriptet.
  • Falsk PATH – Plasser stub-kjørbare filer i $FIXTURE/bin/, og sett denne mappen foran $PATH for å fange opp kall til curl, aws, docker eller andre eksterne verktøy uten å berøre systemets binærfiler.
  • Avgrensning av miljø – Kjør skriptet som testes i et subshell (() eller env -i) slik at endrede variabler aldri lekker tilbake til testkjøreren.
  • Påstander – Små hjelpefunksjoner (assert_eq, assert_file_exists, assert_contains) gir tydelig bestått/ikke bestått-utdata og meningsfulle feilmeldinger.
  • kcov-dekning – Pakker inn kjøringen av skriptet uten endringer i kildekoden og produserer HTML- og JSON-rapporter for linje- og grendekning.
  • Sammenslåing og terskel – Kombiner flere kcov-kjøringer med --merge, analyser JSON-filen og la CI feile hvis dekningen faller under minimumsgrensen.
  • Grendekning kontra linjedekning – Sikt alltid mot grendekning. Linjedekning alene kan overse hele betingede kjørebaner og gi falsk trygghet.

Med disse teknikkene blir Bash-testene Deres like grundige som tester for ethvert kompilert språk.

Gratis å komme i gang

Lær deg Bash med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
22
Leksjoner
88

Ofte stilte spørsmål

Er leksjonen «Testfiksturer, midlertidige miljøer og dekningsgrad» gratis?

Ja – hele teksten i «Testfiksturer, midlertidige miljøer og dekningsgrad» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Mestring av Linux-kommandolinjen og Bash-skripting-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Mestring av Linux-kommandolinjen og Bash-skripting inneholder totalt 4 leksjoner.

Hva lærer jeg i «Testfiksturer, midlertidige miljøer og dekningsgrad»?

Bygg isolerte testfiksturer og mål hvilke grener i skriptene testene faktisk dekker. Du øver på Mestring av Linux-kommandolinjen og Bash-skripting med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Mestring av Linux-kommandolinjen og Bash-skripting?

Ingen tidligere erfaring er nødvendig. Mestring av Linux-kommandolinjen og Bash-skripting på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 3 av 4.

Hvor lang tid tar leksjonen «Testfiksturer, midlertidige miljøer og dekningsgrad»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Mestring av Linux-kommandolinjen og Bash-skripting-leksjonen?

Ja. Alle Mestring av Linux-kommandolinjen og Bash-skripting-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Enhetstesting av funksjoner med Bats-core
  2. Mocking av kommandoer og stubbing av eksterne verktøy
  3. Testfiksturer, midlertidige miljøer og dekningsgrad
  4. Kjøring av skalltester i CI-pipelines
← Tilbake til Mestring av Linux-kommandolinjen og Bash-skripting