mit kcov ungetestete Codepfade sichtbar machen
Wer eine bats-Testsuite fuer Bash-Skripte hat, weiss noch lange nicht, welche Zeilen tatsaechlich ausgefuehrt wurden. Code Coverage mit kcov schliesst diese Luecke, zeigt Zeile fuer Zeile, was Tests wirklich abdecken, und macht ungetestete Fehlerpfade sichtbar, lange bevor sie in Produktion zum Problem werden.
Inhaltsverzeichnis
- 1. Warum Code Coverage auch fuer Shell-Skripte relevant ist
- 2. kcov installieren und erste Messung durchfuehren
- 3. Coverage-Reports lesen: Zeilen, Branches, Funktionen
- 4. Coverage mit bats-Testsuiten kombinieren
- 5. Ungetestete Codepfade identifizieren und schliessen
- 6. Coverage-Schwellenwerte in CI-Pipelines durchsetzen
- 7. bashcov als Alternative zu kcov
- 8. Grenzen von Code Coverage: hohe Zahl, schlechte Tests
- 9. Coverage-Tools im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Code Coverage auch fuer Shell-Skripte relevant ist
In vielen anderen Programmiersprachen ist Code Coverage ein selbstverstaendlicher Bestandteil der Testinfrastruktur, in der Bash-Welt wird die Kennzahl dagegen haeufig ignoriert. Dabei gilt fuer Shell-Skripte genau dasselbe Prinzip wie fuer jede andere Sprache: Eine Testsuite, die nur den Erfolgsfall prueft, aber nie den Fehlerpfad mit ungueltigen Argumenten oder fehlenden Dateien ausfuehrt, hinterlaesst blinde Flecken, die genau dann sichtbar werden, wenn ein Skript in Produktion auf einen unerwarteten Zustand trifft.
Code Coverage fuer Bash-Skripte beantwortet eine einfache, aber wichtige Frage: Welche Zeilen des Skripts wurden waehrend eines Testlaufs tatsaechlich ausgefuehrt, und welche nie? Ein Backup-Skript mit einer Fehlerbehandlung fuer den Fall, dass die Zieldiskette voll ist, mag im Code vorhanden sein, aber wenn kein einziger Testfall diesen Zustand simuliert, bleibt diese Zeile ungetestet und ihr tatsaechliches Verhalten unbekannt, bis der Fehler zum ersten Mal live auftritt.
Das Werkzeug kcov bringt Code Coverage in die Bash-Welt, indem es ein Skript instrumentiert ausfuehrt und dabei protokolliert, welche Zeilen tatsaechlich durchlaufen wurden. Kombiniert mit einer bestehenden bats-Testsuite entsteht so ein vollstaendiges Bild: nicht nur, ob Tests gruen sind, sondern auch, wie viel vom tatsaechlichen Code diese Tests ueberhaupt beruehren. Die folgenden Abschnitte zeigen den kompletten Workflow von der Installation bis zur Durchsetzung von Coverage-Schwellenwerten in der CI-Pipeline.
2. kcov installieren und erste Messung durchfuehren
kcov ist ein eigenstaendiges Kommandozeilenwerkzeug, das Code Coverage fuer kompilierte Binaries und Skriptsprachen misst, ohne dass die zu messende Anwendung selbst angepasst werden muss. Fuer Bash bedeutet das: kcov haengt sich an die Ausfuehrung eines Skripts und protokolliert auf Betriebssystemebene, welche Zeilen tatsaechlich ausgefuehrt wurden, ohne dass das Skript selbst irgendeine Instrumentierung enthalten muss.
Die Installation erfolgt unter Debian und Ubuntu ueber den Paketmanager, alternativ kompiliert man kcov aus dem Quellcode fuer neuere Versionen mit zusaetzlichen Features. Der grundlegende Aufruf kcov ausgabeverzeichnis ./skript.sh fuehrt das Skript ganz normal aus und schreibt gleichzeitig einen vollstaendigen HTML-Coverage-Report in das angegebene Ausgabeverzeichnis, den man anschliessend im Browser oeffnen kann.
# Install kcov on Debian/Ubuntu
sudo apt-get install kcov
# Or build the latest version from source for full Bash support
git clone https://github.com/SimonKagstrom/kcov.git
cd kcov && mkdir build && cd build
cmake .. && make && sudo make install
# Run a script under kcov coverage measurement
kcov --include-path=. ./coverage-output ./deploy.sh
# Open the generated HTML report
xdg-open ./coverage-output/deploy.sh/index.html
# Coverage summary is also printed to stdout, e.g.:
# Percent covered: 73.500
Ein wichtiges Detail: Ohne das Flag --include-path misst kcov auch alle eingebundenen Bibliotheken aus dem System, etwa /usr/lib/bash-interne Funktionen, was den Report unnoetig aufblaeht und die eigentlich interessante Code Coverage des eigenen Skripts verwaesert. Mit --include-path=. beschraenkt sich die Messung auf Dateien im aktuellen Projektverzeichnis, was fuer die meisten Anwendungsfaelle die richtige Einstellung ist.
3. Coverage-Reports lesen: Zeilen, Branches, Funktionen
Der von kcov erzeugte HTML-Report zeigt den Quellcode des Skripts zeilenweise eingefaerbt: gruen fuer Zeilen, die mindestens einmal ausgefuehrt wurden, rot fuer Zeilen, die nie erreicht wurden. Diese einfache visuelle Darstellung macht Code Coverage fuer Bash sofort greifbar, ohne dass man rohe Zahlen interpretieren muss: Ein Blick auf den Report zeigt direkt, welche Fehlerbehandlungsbloecke, welche case-Zweige oder welche Funktionen nie von einem Testfall erreicht wurden.
Neben der reinen Zeilenabdeckung liefert kcov auch Informationen zur Branch Coverage, also der Frage, ob beide Seiten einer Bedingung, sowohl der wahre als auch der falsche Zweig eines if, tatsaechlich durchlaufen wurden. Eine Zeile kann als "ausgefuehrt" gelten, obwohl nur einer von zwei moeglichen Zweigen jemals getestet wurde, ein Detail, das reine Zeilen-Coverage verschleiert, aber fuer die tatsaechliche Testqualitaet entscheidend ist.
# Example: a function with a branch that coverage should reveal as untested
validate_backup_size() {
local size_bytes="$1"
local min_size=$((10 * 1024 * 1024)) # 10 MB minimum
if [[ "$size_bytes" -lt "$min_size" ]]; then
echo "[ERROR] Backup suspiciously small: ${size_bytes} bytes" >&2
return 1
else
echo "[OK] Backup size looks reasonable: ${size_bytes} bytes"
return 0
fi
}
# If tests only ever call validate_backup_size with a large value,
# the kcov report will show the "if" branch (line 6-7) as never
# covered — a real gap, since the error path was never verified.
Der textuelle Zusammenfassungsbericht am Ende eines kcov-Laufs zeigt zusaetzlich eine aggregierte Prozentzahl pro Datei und fuer das gesamte Projekt. Diese Zahl allein sagt aber noch nichts darueber aus, ob die getesteten Zeilen auch tatsaechlich sinnvoll geprueft wurden, ein Punkt, der im Abschnitt ueber die Grenzen von Code Coverage vertieft wird.
4. Coverage mit bats-Testsuiten kombinieren
Der eigentliche Mehrwert von Code Coverage fuer Bash entsteht, wenn kcov nicht ein einzelnes Skript, sondern eine komplette bats-Testsuite instrumentiert. Da bats-core selbst ein Bash-Skript ausfuehrt, kann kcov direkt den bats-Aufruf umschliessen, wodurch die Coverage-Messung ueber alle Testfaelle hinweg aggregiert wird, statt fuer jeden einzelnen Testfall separat gemessen zu werden.
Diese Kombination beantwortet die eigentlich relevante Frage: Welche Zeilen der zu testenden Bibliothek werden von der gesamten Testsuite abgedeckt, ueber alle Testfaelle hinweg? Ein einzelner Testfall deckt vielleicht nur einen kleinen Teil einer Funktion ab, aber zusammen mit weiteren Testfaellen, die andere Codepfade ansprechen, ergibt sich ein vollstaendigeres Bild der tatsaechlichen Testabdeckung.
#!/usr/bin/env bash
# scripts/run-coverage.sh — run the full bats suite under kcov
set -euo pipefail
readonly COVERAGE_DIR="coverage-report"
readonly BATS_BIN="$(command -v bats)"
# kcov wraps the bats binary itself, aggregating coverage
# across every single @test case in the suite
kcov \
--include-path="$(pwd)/lib" \
--exclude-pattern="/test_helper/,/.bats/" \
"$COVERAGE_DIR" \
"$BATS_BIN" test/
echo "Coverage report generated at: ${COVERAGE_DIR}/index.html"
# Extract the aggregated percentage for a quick console summary
grep -o 'covered: [0-9.]*' "${COVERAGE_DIR}/index.html" | head -1
Der Parameter --exclude-pattern ist hier entscheidend: Ohne ihn misst kcov auch die bats-Framework-interne Logik und die Test-Helper-Bibliotheken selbst mit, was die Coverage-Zahl des eigenen Projektcodes verfaelscht. Mit gezieltem Ein- und Ausschluss von Pfaden bleibt der Report auf genau die Dateien beschraenkt, die tatsaechlich Teil der eigenen Bibliothek sind, nicht des Test-Frameworks selbst.
5. Ungetestete Codepfade identifizieren und schliessen
Sobald ein Coverage-Report vorliegt, besteht der naechste Schritt darin, die rot markierten Zeilen systematisch durchzugehen und zu entscheiden, ob es sich um eine echte Testluecke handelt oder um Code, der bewusst nicht getestet werden muss, etwa reine Debug-Ausgaben. Fuer jede echte Luecke wird ein neuer Testfall geschrieben, der genau den Zustand herstellt, der zur Ausfuehrung dieser Zeile fuehrt.
Ein bewaehrtes Vorgehen: Man priorisiert Luecken nach Risiko, nicht nach Menge. Eine ungetestete Fehlerbehandlung in einem Backup-Skript, das im Falle eines Fehlers stillschweigend weiterlaeuft, wiegt schwerer als eine ungetestete Debug-Ausgabe. Code Coverage liefert die Landkarte, aber die Priorisierung, welche Luecken zuerst geschlossen werden, bleibt eine menschliche Entscheidung basierend auf der tatsaechlichen Kritikalitaet des jeweiligen Codepfads.
# lib/backup.sh — the function under scrutiny after a coverage report
backup_database() {
local db_name="$1"
local backup_dir="${2:-/var/backups}"
if [[ ! -d "$backup_dir" ]]; then
echo "[ERROR] Backup directory does not exist: $backup_dir" >&2
return 2
fi
if ! mysqldump "$db_name" > "${backup_dir}/${db_name}.sql" 2>/dev/null; then
echo "[ERROR] mysqldump failed for database: $db_name" >&2
return 3
fi
echo "[OK] Backup created: ${backup_dir}/${db_name}.sql"
return 0
}
#!/usr/bin/env bats
load "test_helper/bats-assert/load"
setup() {
load "../lib/backup.sh"
}
# This test case was added specifically because coverage showed
# the "directory does not exist" branch was never exercised
@test "backup_database fails with exit code 2 for missing backup dir" {
run backup_database "shop" "/nonexistent/path"
assert_equal "$status" 2
assert_output --partial "does not exist"
}
# This test case closes the second gap the coverage report revealed
@test "backup_database fails with exit code 3 when mysqldump fails" {
mysqldump() { return 1; }
export -f mysqldump
run backup_database "shop" "/tmp"
assert_equal "$status" 3
assert_output --partial "mysqldump failed"
}
Nach dem Hinzufuegen dieser beiden Testfaelle zeigt ein erneuter kcov-Lauf beide zuvor roten Zeilen als gruen an, und die aggregierte Coverage-Prozentzahl steigt entsprechend. Wichtig ist dabei, dass die neuen Tests nicht nur die Zeile ausfuehren, sondern auch den korrekten Exit-Code und die korrekte Fehlermeldung pruefen, wie im vorherigen Artikel zu Unit-Tests fuer Bash-Funktionen beschrieben.
6. Coverage-Schwellenwerte in CI-Pipelines durchsetzen
Eine einmalige Coverage-Messung ist nur eine Momentaufnahme. Um zu verhindern, dass Code Coverage mit der Zeit wieder sinkt, weil neue Funktionen ohne begleitende Tests hinzugefuegt werden, laesst sich ein Mindest-Schwellenwert direkt in der CI-Pipeline durchsetzen. Faellt die gemessene Abdeckung unter diesen Wert, schlaegt der Build fehl, bevor der Code gemergt werden kann.
kcov selbst bringt kein eingebautes Schwellenwert-Flag mit, aber die im HTML-Report eingebettete Zusammenfassung laesst sich mit einfachen Shell-Mitteln extrahieren und gegen einen Grenzwert pruefen. Dieses Muster laesst sich direkt in eine GitLab-CI- oder GitHub-Actions-Pipeline integrieren, sodass ein sinkender Coverage-Wert automatisch den Merge-Request blockiert, statt erst spaeter manuell aufzufallen.
#!/usr/bin/env bash
# scripts/check-coverage-threshold.sh — enforce a minimum coverage percentage
set -euo pipefail
readonly MIN_COVERAGE=80
readonly COVERAGE_DIR="coverage-report"
kcov --include-path="$(pwd)/lib" "$COVERAGE_DIR" bats test/ > /dev/null
# Extract the aggregated percentage from kcov's summary output
actual_coverage=$(kcov --include-path="$(pwd)/lib" "$COVERAGE_DIR" bats test/ 2>&1 \
| grep -oP 'Percent covered: \K[0-9.]+' | head -1)
echo "Measured coverage: ${actual_coverage}%"
echo "Required threshold: ${MIN_COVERAGE}%"
if (( $(echo "$actual_coverage < $MIN_COVERAGE" | bc -l) )); then
echo "[ERROR] Coverage ${actual_coverage}% is below the required ${MIN_COVERAGE}% threshold" >&2
exit 1
fi
echo "[OK] Coverage threshold met"
In der Praxis empfiehlt es sich, den Schwellenwert nicht sofort auf einen hohen Wert wie 90 Prozent zu setzen, sondern schrittweise zu erhoehen: zunaechst den aktuellen Ist-Stand als Baseline festlegen, sodass keine bestehende Coverage sinken darf, und den Schwellenwert dann bei jeder Verbesserung der Testsuite manuell anheben. Das verhindert Frustration durch einen von Anfang an unerreichbaren Zielwert, waehrend die Kennzahl trotzdem stetig in die richtige Richtung wandert.
7. bashcov als Alternative zu kcov
bashcov ist eine Ruby-basierte Alternative speziell fuer Bash-Skripte, die auf dem etablierten SimpleCov-Framework aus dem Ruby-Oekosystem aufbaut. Waehrend kcov ein generisches, sprachuebergreifendes Werkzeug ist, das ueber Ptrace-basierte Instrumentierung funktioniert, ist bashcov gezielt fuer Bash-Skripte konzipiert und bringt eine einfachere Installation sowie eine gewohntere HTML-Report-Optik fuer Teams mit, die bereits mit SimpleCov aus anderen Projekten vertraut sind.
Der Nachteil von bashcov: Es erfordert eine Ruby-Umgebung als Abhaengigkeit, was in reinen Bash- oder Container-Umgebungen ohne Ruby einen zusaetzlichen Installationsschritt bedeutet. kcov hingegen ist ein natives C++-Binary ohne Laufzeitabhaengigkeiten, was es fuer schlanke CI-Container oft praktischer macht. Beide Werkzeuge liefern aber prinzipiell dieselbe Kernfunktion, Zeilen-Coverage-Messung fuer Bash-Skripte ohne Codeaenderung am zu messenden Skript.
# Install bashcov via RubyGems (requires Ruby to be installed)
gem install bashcov
# Run a script or test suite under bashcov measurement
bashcov ./deploy.sh
# Run an entire bats test suite under bashcov
bashcov -- bats test/
# bashcov generates a SimpleCov-style HTML report in ./coverage/
xdg-open ./coverage/index.html
Fuer Teams, die bereits Ruby-Tooling im Einsatz haben, etwa fuer andere Testframeworks oder Deployment-Skripte, kann bashcov die pragmatischere Wahl sein. Fuer reine Bash- oder Go-lastige Umgebungen ohne bestehende Ruby-Abhaengigkeit bleibt kcov meist die schlankere Option, gerade in minimalen Docker-Images fuer CI-Pipelines.
8. Grenzen von Code Coverage: hohe Zahl, schlechte Tests
Eine hohe Code-Coverage-Zahl ist keine Garantie fuer gute Tests. Ein Testfall, der eine Funktion aufruft, aber keine einzige Assertion auf das Ergebnis enthaelt, erzeugt technisch gesehen hundert Prozent Zeilen-Coverage fuer diese Funktion, ohne tatsaechlich zu pruefen, ob das Verhalten korrekt ist. Coverage misst, welcher Code ausgefuehrt wurde, nicht, ob das Ergebnis dieser Ausfuehrung tatsaechlich validiert wurde.
Ein zweites Problem: Coverage sagt nichts ueber die Qualitaet der getesteten Eingabewerte aus. Eine Funktion, die mit demselben Eingabewert zehnmal aufgerufen wird, erreicht dieselbe Zeilen-Coverage wie eine Funktion, die mit zehn unterschiedlichen, sorgfaeltig ausgewaehlten Grenzfaellen getestet wird, obwohl die zweite Testsuite erheblich wertvoller ist. Aus diesem Grund sollte Code Coverage immer als notwendige, aber nicht hinreichende Bedingung fuer gute Tests verstanden werden, ergaenzt um manuelle Reviews der tatsaechlichen Assertions in jedem Testfall.
9. Coverage-Tools im Vergleich
Je nach Projektumgebung und bereits vorhandenem Tooling eignen sich kcov, bashcov und eine rein manuelle Coverage-Analyse unterschiedlich gut.
| Werkzeug | Abhaengigkeiten | Report-Format | Am besten geeignet fuer |
|---|---|---|---|
| kcov | Keine Laufzeitabhaengigkeit | HTML, Cobertura XML | Schlanke CI-Container, sprachuebergreifende Projekte |
| bashcov | Benoetigt Ruby | SimpleCov HTML | Teams mit bestehendem Ruby-Tooling |
| Manuelle Analyse | Keine | Keine strukturierte Ausgabe | Sehr kleine, einmalige Skripte |
| ShellCheck (statisch) | Keine | Warnungen, keine Coverage | Ergaenzung, kein Coverage-Ersatz |
In der Praxis ist kcov fuer die meisten Bash-Projekte die pragmatischste Wahl, da es ohne zusaetzliche Laufzeitumgebung auskommt und sich problemlos in bestehende CI-Container integrieren laesst. bashcov lohnt sich vor allem dort, wo Ruby ohnehin bereits Teil des Toolchains ist. ShellCheck ersetzt keine Coverage-Messung, ergaenzt sie aber sinnvoll, da es Probleme wie unquotierte Variablen bereits vor der Testausfuehrung statisch findet.
Mironsoft
Shell-Automatisierung, Testing und Deployment-Infrastruktur
Ungetestete Codepfade in Bash-Skripten aufdecken?
Wir richten kcov-basierte Code-Coverage-Messung fuer eure bats-Testsuiten ein, definieren realistische CI-Schwellenwerte und schliessen gezielt die wichtigsten Testluecken in kritischen Deployment- und Backup-Skripten.
Coverage-Setup
kcov-Integration mit bestehenden bats-Testsuiten
Luecken schliessen
Priorisierte Testfaelle fuer kritische, ungetestete Fehlerpfade
CI-Integration
Coverage-Schwellenwerte in GitLab CI und GitHub Actions durchsetzen
10. Zusammenfassung
Code Coverage fuer Bash-Skripte macht sichtbar, was reine Testergebnisse verschweigen: welche Zeilen tatsaechlich ausgefuehrt wurden und welche seit dem Schreiben des Codes nie einen einzigen Testlauf durchlaufen haben. kcov instrumentiert Bash-Skripte ohne Codeaenderung, erzeugt farbcodierte HTML-Reports und laesst sich direkt mit bestehenden bats-Testsuiten kombinieren, um die Abdeckung ueber alle Testfaelle hinweg zu aggregieren.
Coverage-Schwellenwerte in der CI-Pipeline verhindern schleichenden Rueckgang der Testqualitaet, waehrend priorisierte Ergaenzung von Testfaellen fuer die riskantesten ungetesteten Codepfade den groessten Sicherheitsgewinn bringt. Wichtig bleibt dabei die Erkenntnis, dass eine hohe Coverage-Zahl allein noch keine gute Testsuite garantiert, solange die tatsaechlichen Assertions in jedem Testfall nicht ebenfalls kritisch geprueft werden.
Code Coverage fuer Bash-Skripte — Das Wichtigste auf einen Blick
kcov
Instrumentiert Bash-Skripte ohne Codeaenderung, erzeugt farbcodierte HTML-Reports, keine Laufzeitabhaengigkeit.
bats-Kombination
kcov um den bats-Aufruf legen, um Coverage ueber die gesamte Testsuite hinweg zu aggregieren.
CI-Schwellenwerte
Mindestwert durchsetzen, schrittweise erhoehen, statt sofort einen unerreichbaren Zielwert zu setzen.
Grenzen kennen
Hohe Zeilen-Coverage ersetzt keine sorgfaeltigen Assertions und keine durchdachten Testfaelle.