Fixtures, środowiska tymczasowe i pokrycie kodu
Twórz izolowane fixtures testowe i mierz, które gałęzie skryptu są faktycznie wykonywane przez testy.
Fixtures, środowiska tymczasowe i pokrycie kodu to bezpłatna lekcja Linux Command Line & Bash Scripting Mastery na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Linux Command Line & Bash Scripting Mastery, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Linux Command Line & Bash Scripting Mastery zawiera 4 lekcji w sumie.
Dlaczego elementy fixture i izolacja mają znaczenie
Podczas testowania skryptów Bash największym zagrożeniem są efekty uboczne: testy mogą przypadkowo modyfikować rzeczywiste pliki, bazy danych lub stan systemu. Test, który przechodzi na Państwa komputerze, ale uszkadza dane produkcyjne, jest gorszy niż całkowity brak testów.
Rozwiązaniem są fixture testowe — kontrolowane, jednorazowe środowiska odwzorowujące rzeczywiste warunki bez dotykania prawdziwych zasobów. Dobre fixture zapewniają:
- Powtarzalność — testy dają ten sam wynik przy każdym uruchomieniu
- Izolację — testy nie wpływają na siebie nawzajem ani na hosta
- Bezpieczeństwo — operacje destrukcyjne dotyczą wyłącznie danych przeznaczonych do usunięcia
- Szybkość — brak wywołań sieciowych i intensywnych operacji wejścia-wyjścia, chyba że są absolutnie konieczne
W testach Bash fixture to zazwyczaj katalogi tymczasowe wypełnione znanymi plikami, atrapowe pliki wykonywalne umieszczone na początku $PATH oraz zmienne środowiskowe ograniczone do procesu testowego.
Tworzenie i usuwanie katalogów tymczasowych
Standardowy wzorzec katalogu tymczasowego dla pojedynczego testu wykorzystuje mktemp -d, które tworzy unikalny katalog w /tmp i wypisuje jego ścieżkę. Zapisz tę ścieżkę i zarejestruj trap, aby katalog został automatycznie usunięty przy zakończeniu powłoki — nawet w przypadku błędu.
Ten idiom składający się z dwóch wierszy powinien znajdować się w każdym pliku testowym, który korzysta z systemu plików:
#!/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'Strukturyzowanie drzewa katalogów fixture
Dobrze uporządkowany fixture odwzorowuje układ katalogów, którego rzeczywiście oczekuje testowany skrypt. Należy traktować go jako miniaturowy, fikcyjny katalog główny projektu. Funkcja konfigurująca fixture tworzy ten układ przed każdym testem, a procedura sprzątająca go usuwa.
Najważniejsze praktyki:
- Używaj funkcji
setup(), którą framework testowy wywołuje przed każdym testem - Używaj funkcji
teardown()lubcleanup(), uruchamianej po każdym teście, także w przypadku błędu - Pliki fixture powinny być minimalne — zawieraj tylko to, co skrypt rzeczywiście odczytuje
- Nazywaj pliki fixture opisowo, aby łatwo można było diagnozować błędy
#!/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_fixtureMockowanie plików wykonywalnych za pomocą fikcyjnego PATH
Wiele skryptów Bash wywołuje zewnętrzne narzędzia, takie jak curl, aws, docker czy git. W testach nie chcemy łączyć się z rzeczywistymi usługami, dlatego zastępujemy te narzędzia fikcyjnymi plikami wykonywalnymi.
Technika jest prosta:
- Utwórz tymczasowy katalog
bin/wewnątrz fixture - Umieść w nim małe skrypty powłoki o takich samych nazwach jak rzeczywiste narzędzia
- Dodaj ten katalog na początku
$PATHprzed wywołaniem testowanego skryptu
Ponieważ $PATH jest przeszukiwany od lewej do prawej, wygrywa fikcyjny plik. Rzeczywisty plik binarny nigdy nie zostanie wywołany.
#!/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"Przechwytywanie i sprawdzanie danych wyjściowych poleceń
Fixture jest użyteczny tylko wtedy, gdy można sprawdzić, co zrobił skrypt. Standardowe wzorce to:
- Przechwytywanie stdout/stderr do zmiennych za pomocą
$()lub podstawienia procesu - Sprawdzanie plików dziennika zapisanych przez fikcyjne pliki binarne
- Jawne sprawdzanie kodów zakończenia za pomocą
$?lub logiki warunkowej - Weryfikowanie, czy określone pliki zostały utworzone, zmodyfikowane lub pozostały nietknięte
Tworzenie małych, wyspecjalizowanych funkcji pomocniczych do asercji sprawia, że testy są czytelne, a komunikaty o błędach — precyzyjne.
#!/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"Zakres zmiennych środowiskowych w testach
Skrypty często odczytują zmienne środowiskowe, takie jak $HOME, $CONFIG_PATH czy $DATABASE_URL. W testach trzeba je nadpisywać bez zanieczyszczania rzeczywistego środowiska.
Najbezpieczniej uruchamiać testowany skrypt w podpowłoce, przekazując tylko jawnie ustawione zmienne. Polecenie env pozwala wyczyścić środowisko i dodać ponownie wyłącznie potrzebne elementy:
env -i VAR=val ./script.sh— całkowicie czyste środowisko(export VAR=val; ./script.sh)— podpowłoka dziedziczy środowisko procesu nadrzędnego oraz Państwa nadpisania
Używanie podpowłok oznacza również, że jeśli skrypt zmieni $IFS, $PWD lub inny globalny stan, zmiany te nie wydostaną się z powrotem do procesu uruchamiającego testy.
#!/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"Wprowadzenie do pokrycia kodu Bash za pomocą kcov
Pokrycie kodu odpowiada na pytanie: które wiersze (i gałęzie) mojego skryptu zostały rzeczywiście wykonane przez testy? Wysoki poziom pokrycia nie gwarantuje poprawności, ale niski ujawnia nieprzetestowane ścieżki, w których prawdopodobnie kryją się błędy.
Głównym narzędziem do pomiaru pokrycia kodu Bash jest kcov. Działa ono poprzez instrumentowanie skryptu na poziomie systemu operacyjnego za pomocą PTRACE (Linux) lub dtrace (macOS), dlatego nie wymaga zmian w kodzie źródłowym. Tworzy raport HTML pokazujący wiersze czerwone (niepokryte) i zielone (pokryte).
Podstawowe użycie:
kcov --include-path=./src coverage-out/ ./src/myscript.sh- Otwórz
coverage-out/index.htmlw przeglądarce, aby przejrzeć wyniki - W CI przeanalizuj
coverage-out/myscript.sh/coverage.json, aby uzyskać procentową wartość w formacie przeznaczonym do odczytu maszynowego
Uwaga: kcov trzeba zainstalować osobno (brew install kcov w macOS, apt install kcov w Ubuntu 20.04+).
Uruchamianie kcov dla rzeczywistego skryptu
Oto kompletny przykład przedstawiający skrypt gotowy do wdrożenia, test, który go wykonuje, oraz wywołanie kcov mierzące pokrycie. Zwróć uwagę, że katalog wyjściowy jest tworzony osobno dla każdego testu, dzięki czemu później można scalać wyniki z wielu uruchomień testów.
#!/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"Scalanie pokrycia z wielu uruchomień testów
Pojedynczy test rzadko obejmuje wszystkie gałęzie. Uruchamiasz wiele testów — każdy we własnym katalogu wyników pokrycia — a następnie je scalasz. Flaga --merge narzędzia kcov łączy wiele uruchomień w jeden ujednolicony raport.
Typowy wzorzec w potoku CI:
- Uruchom test A → zapisz wynik w
cov/test_a/ - Uruchom test B → zapisz wynik w
cov/test_b/ - Scal →
kcov --merge cov/all/ cov/test_a/ cov/test_b/ - Przeanalizuj
cov/all/<script>/coverage.json, aby uzyskać końcową wartość procentową
Można również wymusić minimalny próg i zakończyć budowanie CI błędem, jeśli pokrycie spadnie poniżej tej wartości:
#!/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"Pokrycie gałęzi a pokrycie wierszy
Można spotkać dwa główne wskaźniki pokrycia:
- Pokrycie wierszy — czy dany wiersz został w ogóle wykonany? Łatwo nim manipulować: pojedynczy test może wykonać wiele wierszy, pomijając ważne ścieżki warunkowe.
- Pokrycie gałęzi — czy wykonano każdą gałąź każdego wyrażenia
if,caseoraz&&/||? To znacznie lepszy wskaźnik. Wymaga testów zarówno dla prawdziwej, jak i fałszywej strony każdej decyzji.
kcov raportuje oba wskaźniki. Najważniejszy wniosek: 100% pokrycia wierszy nie oznacza 100% pokrycia gałęzi. Rozważmy ten skrypt — pojedynczy test z niepustym plikiem obejmie każdy wiersz, ale gałąź dla pustego pliku (wiersz 9 poniżej) nigdy nie zostanie osiągnięta:
#!/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")Integracja danych testowych i pokrycia kodu w CI
Wszystkie elementy razem: solidny potok CI dla projektów Bash łączy przygotowanie danych testowych, wykonywanie testów z użyciem kcov, scalanie oraz sprawdzanie progu w jednym skrypcie. Skrypt ten staje się punktem wejścia CI — jedno polecenie uruchamia całość.
Zasady projektowe uruchamiacza testów CI:
- Każdy przypadek testowy wywołuje
setup_fixturei rejestrujeteardown_fixtureza pomocątrap - Testowany skrypt jest uruchamiany ze sztucznym
$PATHi ograniczonym zakresem zmiennych środowiskowych - kcov otacza każde wywołanie, zapisując wyniki w numerowanym podkatalogu
- Po zakończeniu wszystkich testów kcov scala wyniki, a skrypt sprawdzający próg blokuje proces budowania w razie niespełnienia wymagań
- Runner CI kończy działanie kodem różnym od zera, jeśli dowolny test lub sprawdzenie pokrycia zakończy się niepowodzeniem
#!/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 ))Sprawdzenie wiedzy: technika sztucznego PATH
Proszę sprawdzić swoje rozumienie techniki sztucznego PATH używanej w danych testowych Bash.
Podsumowanie: dane testowe, tymczasowe środowiska i pokrycie kodu
Ta lekcja obejmowała pełny zestaw narzędzi do rzetelnego, izolowanego testowania Bash:
- Katalogi tymczasowe —
mktemp -dwraz ztrap ... EXITgwarantują automatyczne sprzątanie niezależnie od sposobu zakończenia testu. - Struktura danych testowych — para
setup_fixture/teardown_fixturetworzy i usuwa minimalne drzewo katalogów odwzorowujące rzeczywiste dane wejściowe skryptu. - Sztuczny PATH — należy umieścić wykonywalne atrapy w
$FIXTURE/bin/i dodać ten katalog na początku$PATH, aby przechwytywać wywołaniacurl,aws,dockerlub dowolnego zewnętrznego narzędzia bez modyfikowania plików systemowych. - Izolowanie środowiska — należy uruchamiać testowany skrypt w podpowłoce (
()lubenv -i), aby zmienione zmienne nigdy nie przedostawały się z powrotem do uruchamiacza testów. - Asercje — małe funkcje pomocnicze (
assert_eq,assert_file_exists,assert_contains) generują czytelne informacje o powodzeniu lub niepowodzeniu oraz znaczące komunikaty o błędach. - Pokrycie kodu za pomocą kcov — kcov otacza wykonywanie skryptu bez zmian w kodzie źródłowym i tworzy raporty HTML oraz JSON dotyczące pokrycia linii i gałęzi.
- Scalanie i próg — należy połączyć wiele uruchomień kcov za pomocą
--merge, przeanalizować plik JSON i zakończyć CI niepowodzeniem, jeśli pokrycie spadnie poniżej ustalonego minimum. - Pokrycie gałęzi a pokrycie linii — zawsze należy dążyć do pokrycia gałęzi; samo pokrycie linii może pomijać całe ścieżki warunkowe i dawać fałszywe poczucie bezpieczeństwa.
Dzięki tym technikom testy Bash stają się równie rygorystyczne jak testy dowolnego języka kompilowanego.
Często zadawane pytania
Czy lekcja „Fixtures, środowiska tymczasowe i pokrycie kodu” jest bezpłatna?
Tak — pełny tekst „Fixtures, środowiska tymczasowe i pokrycie kodu” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Linux Command Line & Bash Scripting Mastery, przejdź na CoddyKit PRO. Kurs Linux Command Line & Bash Scripting Mastery zawiera 4 lekcji w sumie.
Co nauczysz się w „Fixtures, środowiska tymczasowe i pokrycie kodu”?
Twórz izolowane fixtures testowe i mierz, które gałęzie skryptu są faktycznie wykonywane przez testy. Ćwiczysz Linux Command Line & Bash Scripting Mastery z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Linux Command Line & Bash Scripting Mastery?
Nie wymagamy żadnego doświadczenia. Linux Command Line & Bash Scripting Mastery w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Fixtures, środowiska tymczasowe i pokrycie kodu”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Linux Command Line & Bash Scripting Mastery?
Tak. Każda lekcja Linux Command Line & Bash Scripting Mastery zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Testowanie jednostkowe funkcji za pomocą Bats-core
- Mockowanie poleceń i zastępowanie narzędzi zewnętrznych
- Fixtures, środowiska tymczasowe i pokrycie kodu
- Uruchamianie testów powłoki w potokach CI