Linting, BATS-Framework, Mocking und CI-Integration
Shell-Skripte werden selten getestet — und genau deshalb scheitern sie ohne Vorwarnung in der Produktion. ShellCheck findet statische Fehler bevor das Skript läuft. Das BATS-Framework ermöglicht echte Unit-Tests für Bash-Funktionen. Mocking-Strategien isolieren externe Befehle. CI-Integration stellt sicher, dass jeder Commit diesen Standard einhält.
Inhaltsverzeichnis
- 1. Warum Bash-Skripte getestet werden müssen
- 2. ShellCheck: Installation, Konfiguration und Grundbetrieb
- 3. Die wichtigsten ShellCheck-Regeln im Detail
- 4. BATS-Framework: Grundlagen und Teststruktur
- 5. Mocking: externe Befehle und Systemaufrufe isolieren
- 6. Assertions und Helpers in BATS
- 7. Coverage: Testabdeckung für Shell-Skripte messen
- 8. CI-Integration: GitHub Actions und GitLab CI
- 9. ShellCheck vs. BATS: Wann welches Werkzeug?
- 10. Zusammenfassung
- 11. FAQ
1. Warum Bash-Skripte getestet werden müssen
Bash-Skripte tragen in vielen Projekten eine unverhältnismäßig hohe operative Verantwortung — Deployments, Backups, Konfigurationsmanagement, Datenmigration — und werden gleichzeitig am seltensten getestet. Die typische Begründung: "Es ist nur ein Shell-Skript." Das ist eine gefährliche Unterschätzung. Ein Shell-Skript, das fehlerhaft läuft, löscht Dateien, schreibt falsche Konfigurationen oder deployed eine defekte Version in die Produktion. Die Konsequenz ist dieselbe wie bei jedem anderen fehlerhaften Programm — nur ohne die Sicherheitsnetze, die in anderen Sprachen üblich sind.
ShellCheck und das BATS-Framework sind die zwei Werkzeuge, die zusammen eine vollständige Qualitätssicherungsstrategie für Bash-Skripte bilden. ShellCheck ist ein statischer Analyzer, der Code-Muster erkennt, die zu fehlerhaftem Verhalten führen — ohne das Skript ausführen zu müssen. BATS (Bash Automated Testing System) ist ein Test-Framework, das echte Ausführung von Funktionen und Skripten mit Assertions ermöglicht. Beide Werkzeuge ergänzen sich: ShellCheck findet strukturelle Fehler, BATS verifiziert tatsächliches Laufzeitverhalten.
Die Integration von ShellCheck in CI/CD-Pipelines kostet wenige Minuten Einrichtungszeit und bringt sofortigen Mehrwert: Jeder Commit, der ein Shell-Skript mit einer der häufigen Fehlerklassen einführt, schlägt im CI fehl — bevor der Code in die Hauptlinie gemergt wird. Das ist genau der Zeitpunkt, an dem Fehler am kostengünstigsten zu beheben sind. Wer einmal erlebt hat, wie ein ungetestetes Deployment-Skript in der Produktion verwaiste Dateien hinterlassen hat, weil --delete ohne vorherige Prüfung lief, versteht die Motivation für ShellCheck und BATS.
2. ShellCheck: Installation, Konfiguration und Grundbetrieb
ShellCheck ist auf den meisten Systemen direkt über den Package-Manager verfügbar: apt install shellcheck, brew install shellcheck oder als Binary-Download. Die einfachste Verwendung: shellcheck skript.sh — ShellCheck analysiert die Datei und gibt Warnungen mit Kategorien (error, warning, info, style) und Links zur Dokumentation aus. Jede Warnung hat eine Nummer (z.B. SC2086) und einen erklärenden Text, der nicht nur den Fehler beschreibt, sondern auch das korrekte Muster zeigt.
Die ShellCheck-Konfiguration kann project-weit über eine .shellcheckrc-Datei gesteuert werden: shell=bash legt die Ziel-Shell fest, disable=SC2034 deaktiviert bestimmte Regeln projekt-weit. Regeln können auch inline im Skript deaktiviert werden: # shellcheck disable=SC2086 vor der betroffenen Zeile. Das sollte sparsam eingesetzt werden — wenn man eine Warnung deaktiviert, muss man begründen können, warum der Code trotzdem korrekt ist. Für CI/CD eignet sich die Ausgabe im --format=checkstyle-Format, das von allen gängigen CI-Systemen als Fehler-Report interpretiert werden kann.
# .shellcheckrc — Project-wide ShellCheck configuration
shell=bash
enable=all
# Disable globally only when justified:
# disable=SC2034 # unused variable (sometimes intentional in lib files)
# --- Minimal Makefile integration ---
# make lint → runs ShellCheck on all .sh files
# make test → runs BATS test suite
3. Die wichtigsten ShellCheck-Regeln im Detail
Die häufigsten und wirkungsvollsten ShellCheck-Regeln sind SC2086 (unquoted variable), SC2155 (local combined with command substitution), SC2006 (backtick usage statt $()) und SC2181 (Überprüfung von $? statt direkter Bedingung). SC2086 ist mit Abstand die häufigste Fundstelle: $variable ohne Anführungszeichen führt zu Word-Splitting und Glob-Expansion, was bei Strings mit Leerzeichen, Wildcards oder Newlines zu komplett unerwartetem Verhalten führt. ShellCheck findet dieses Muster zuverlässig in jeder Datei.
SC2155 (local result=$(cmd)) ist die zweithäufigste kritische Fundstelle: Das local-Builtin schluckt den Exit-Code der Subshell, wodurch set -e nicht mehr greift. SC2164 (cd ... || exit) warnt, wenn auf ein cd keine Fehlerbehandlung folgt — ein klassischer Fehler, bei dem das Skript im falschen Verzeichnis weitermacht. SC2046 warnt vor unquoted Befehlssubstitution. SC2068 und SC2145 decken fehlerhafte Array-Expansionen auf. ShellCheck dokumentiert jede Regel unter https://www.shellcheck.net/wiki/SCxxxx mit ausführlicher Erklärung und Beispielen für das korrekte Muster.
4. BATS-Framework: Grundlagen und Teststruktur
BATS (Bash Automated Testing System) ist ein Test-Framework für Bash-Skripte, das nach dem Prinzip "one test per @test block" arbeitet. Jeder Test ist eine Funktion mit dem Namen nach @test "Beschreibung". Der Test gilt als bestanden, wenn alle Befehle mit Exit-Code 0 enden. BATS bietet Helpers wie run (Befehl ausführen und Ausgabe in $output, Exit-Code in $status speichern) und die Helper-Library bats-assert für lesbare Assertions wie assert_success, assert_failure, assert_output.
Die ShellCheck-Konfiguration gilt auch für BATS-Testdateien. BATS-Tests werden als .bats-Dateien gespeichert, typischerweise in einem test/-Verzeichnis. Die Installation erfolgt als git-Submodul oder per Package-Manager. Für Library-Testing werden Shell-Funktionen aus der zu testenden Datei per source eingebunden, dann einzeln aufgerufen und verifiziert. Das setup()-Hook läuft vor jedem Test, teardown() danach — ideal für temporäre Verzeichnisse und Cleanup. BATS ergänzt ShellCheck um die Dimension des Laufzeitverhaltens: Was ShellCheck statisch prüft, verifiziert BATS dynamisch.
#!/usr/bin/env bats
# test/deploy.bats — BATS tests for deployment functions
# Install: git submodule add https://github.com/bats-core/bats-core test/bats
# Run: ./test/bats/bin/bats test/
# Load helpers and the function under test
load 'bats-support/load'
load 'bats-assert/load'
# shellcheck source=../lib/deploy.sh
load '../lib/deploy.sh'
setup() {
# Create isolated temp dir for each test
TEST_TMPDIR="$(mktemp -d)"
export TEST_TMPDIR
}
teardown() {
rm -rf "$TEST_TMPDIR"
}
@test "get_release_name returns timestamp-based name" {
run get_release_name
assert_success
assert_output --regexp '^[0-9]{8}-[0-9]{6}$'
}
@test "validate_config fails when DEPLOY_HOST is unset" {
unset DEPLOY_HOST
run validate_config
assert_failure
assert_output --partial "DEPLOY_HOST"
}
@test "create_release_dir creates directory structure" {
run create_release_dir "$TEST_TMPDIR/releases" "20260509-120000"
assert_success
assert [ -d "$TEST_TMPDIR/releases/20260509-120000" ]
}
@test "prune_releases keeps only last N releases" {
# Create 7 fake release directories
for i in $(seq 1 7); do
mkdir -p "$TEST_TMPDIR/releases/2026050${i}-120000"
done
run prune_releases "$TEST_TMPDIR/releases" 5
assert_success
local count
count=$(find "$TEST_TMPDIR/releases" -maxdepth 1 -type d | wc -l)
assert [ "$count" -eq 6 ] # 5 releases + parent dir
}
5. Mocking: externe Befehle und Systemaufrufe isolieren
Das größte Hindernis beim Testen von Shell-Skripten sind externe Befehle: ssh, rsync, curl, aws, Datenbankclients. In Unit-Tests sollen diese Befehle nicht wirklich ausgeführt werden — man will das Verhalten des Skripts bei bestimmten Ausgaben und Exit-Codes testen, ohne dass echte Netzwerkaufrufe oder Dateioperationen stattfinden. Mocking in Bash-Tests funktioniert durch das Überschreiben externer Befehle mit Shell-Funktionen, die im aktuellen Scope definiert werden. Die Funktion hat denselben Namen wie der externe Befehl und gibt definierte Ausgabe und Exit-Codes zurück.
In BATS-Tests werden Mock-Funktionen in setup() definiert und in teardown() durch unset -f befehlsname wieder entfernt. Das Skript-Under-Test muss so strukturiert sein, dass externe Befehle in Funktionen gekapselt sind, die gemockt werden können. Das ist gleichzeitig ein Qualitätskriterium für das Design von Shell-Skripten: Wer sein Skript testbar schreiben will, strukturiert es so, dass externe Abhängigkeiten isoliert sind — was das Skript auch in anderen Kontexten wartbarer macht. ShellCheck und Tests zusammen führen zu besser strukturierten Bash-Skripten.
#!/usr/bin/env bats
# test/deploy-mocking.bats — Mocking external commands in BATS tests
load 'bats-support/load'
load 'bats-assert/load'
load '../lib/deploy.sh'
setup() {
TEST_TMPDIR="$(mktemp -d)"
export TEST_TMPDIR
export DEPLOY_LOG="$TEST_TMPDIR/deploy.log"
export DRY_RUN=0
}
teardown() {
# Remove mock functions — unset restores real commands
unset -f rsync ssh curl
rm -rf "$TEST_TMPDIR"
}
# Mock rsync: simulate successful transfer with stats output
rsync() {
echo "sent 1,234 bytes received 56 bytes"
echo "Number of files: 10"
return 0
}
export -f rsync
# Mock ssh: capture command that would be executed
ssh() {
echo "MOCK SSH: $*" >> "$TEST_TMPDIR/ssh_calls.log"
return 0
}
export -f ssh
# Mock curl: simulate network timeout
curl_timeout() {
return 28 # curl exit code for timeout
}
@test "deploy runs rsync and logs transfer stats" {
run deploy_files "/tmp/src/" "server:/var/www/"
assert_success
assert_output --partial "sent 1,234 bytes"
}
@test "deploy aborts when network check fails" {
# Override curl with timeout mock for this test
curl() { return 28; }
export -f curl
run check_network_connectivity "https://example.com"
assert_failure
assert_output --partial "timeout"
}
@test "symlink_swap creates correct link on remote" {
run symlink_swap "server" "/var/www/releases/20260509" "/var/www/current"
assert_success
grep -q "ln -sfn" "$TEST_TMPDIR/ssh_calls.log"
}
6. Assertions und Helpers in BATS
Die Standard-BATS-Assertions sind minimalistisch — nur Exit-Code und String-Matching. Die Hilfsbibliothek bats-assert erweitert sie um lesbare Assertions: assert_success, assert_failure, assert_output "erwarteter Text", assert_output --regexp 'pattern', assert_line --index 0 "erste Zeile", refute_output "unerwarteter Text". Mit bats-file kommen Datei-Assertions hinzu: assert_file_exists, assert_file_contains, assert_symlink_to. Diese Assertions machen Test-Ausgaben bei Fehlern deutlich lesbarer als rohe assert [ ... ]-Vergleiche.
Die ShellCheck-kompatible Teststruktur für maximale Lesbarkeit: Setup-Funktionen für komplexe Umgebungen, Helper-Funktionen für häufig verwendete Assertion-Kombinationen und klare Test-Namen, die das erwartete Verhalten beschreiben. Test-Namen wie "deploy_files aborts when source directory is missing" sind selbstdokumentierend — wenn der Test fehlschlägt, ist sofort klar, was nicht funktioniert. Diese Konvention ist besonders wichtig, wenn Tests in CI/CD-Pipelines laufen und Entwickler die Fehler im Pipeline-Log lesen müssen, ohne lokalen Zugriff auf die Umgebung zu haben.
7. Coverage: Testabdeckung für Shell-Skripte messen
Coverage für Shell-Skripte ist weniger etabliert als in anderen Sprachen, aber mit bashcov (basierend auf SimpleCov) oder dem Ansatz über set -x und Trace-Auswertung messbar. ShellCheck deckt strukturelle Schwächen auf, aber Coverage zeigt, welche Code-Pfade unter welchen Bedingungen durchlaufen werden. Für kritische Skripte — Deployments, Backup-Rotationen, Datenmigrationen — ist Coverage-Messung sinnvoll, um sicherzustellen, dass Fehler-Pfade, Cleanup-Funktionen und alternative Ausführungszweige auch getestet werden.
Der pragmatische Ansatz für Coverage in der Praxis: Statt Prozent-Metriken die kritischen Code-Pfade explizit auflisten und für jeden einen BATS-Test schreiben. "Der ERR-Trap wird aufgerufen wenn rsync fehlschlägt." "Die Cleanup-Funktion löscht das Lockfile auch wenn das Skript durch SIGTERM beendet wird." "Die Backup-Rotation lässt exakt 7 Releases übrig." Diese strukturierte Vorgabe ersetzt die prozentuale Coverage-Metrik und ist für Shell-Skripte praktikabler. ShellCheck in Kombination mit dieser Checkliste bildet das vollständige Qualitätssicherungssystem.
8. CI-Integration: GitHub Actions und GitLab CI
Die Integration von ShellCheck in CI-Pipelines ist einer der einfachsten und wirkungsvollsten Schritte für die Qualität von Shell-Skripten. In GitHub Actions gibt es die offizielle shellcheck-action, die alle .sh-Dateien im Repository automatisch analysiert. In GitLab CI ist ein einfacher Job mit image: koalaman/shellcheck-alpine die schnellste Lösung. Der Job schlägt fehl, wenn ShellCheck Fehler der Kategorie "error" oder "warning" findet — und gibt für jeden Entwickler im Merge-Request-Kommentar die betroffenen Zeilen aus.
Die empfohlene CI-Konfiguration für maximale Effektivität: ShellCheck mit --severity=warning für obligatorische Prüfungen (schlägt CI fehl) und BATS als separater Job, der parallel zum ShellCheck-Job läuft. So gibt es in der Pipeline immer zwei Prüfebenen: statische Analyse durch ShellCheck und dynamische Tests durch BATS. Für Projekte mit vielen Shell-Skripten lohnt auch ein Pre-Commit-Hook, der ShellCheck lokal ausführt — so werden Fehler noch früher erkannt, bevor sie überhaupt ins Repository gelangen.
9. ShellCheck vs. BATS: Wann welches Werkzeug?
Die Stärken von ShellCheck und BATS liegen in verschiedenen Dimensionen der Qualitätssicherung. Beide zusammen bilden das vollständige Werkzeugset für professionelle Bash-Skript-Entwicklung.
| Dimension | ShellCheck | BATS | Empfehlung |
|---|---|---|---|
| Fehlertyp | Statische Code-Muster | Laufzeitverhalten | Beide parallel in CI |
| Einrichtungsaufwand | Minimal (apt install) | Mittel (Submodule, Helpers) | ShellCheck zuerst, BATS für kritische Skripte |
| Ausführungszeit | Sekunden | Sekunden bis Minuten | Parallel als separate CI-Jobs |
| Mocking nötig? | Nein | Ja, für externe Befehle | Mock-Strategie früh definieren |
| Pre-Commit-Hook geeignet | Ideal — sehr schnell | Möglich für schnelle Unit-Tests | ShellCheck als Pre-Commit, BATS in CI |
ShellCheck sollte in jedem Projekt mit Shell-Skripten aktiv sein — es gibt keine sinnvolle Begründung, auf den kostenlosen statischen Analyzer zu verzichten. BATS ist die nächste Investitionsstufe für Skripte mit signifikanter Business-Logik oder hohem Risikopotential. Ein Deployment-Skript, das Produktionsdaten verwaltet, verdient BATS-Tests. Ein einzeiliger Wrapper-Skript für einen häufig genutzten Befehl genügt mit ShellCheck.
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Shell-Skripte mit echten Tests und automatischem Linting?
Wir integrieren ShellCheck und BATS in eure CI/CD-Pipeline, schreiben Tests für kritische Deployment-Skripte und implementieren Mocking-Strategien für externe Abhängigkeiten.
ShellCheck-Setup
CI-Integration, .shellcheckrc und Pre-Commit-Hooks konfigurieren
BATS-Test-Suite
Unit-Tests für kritische Bash-Funktionen mit Mocking-Framework aufbauen
CI/CD-Pipeline
ShellCheck + BATS als parallele CI-Jobs in GitHub Actions oder GitLab CI
10. Zusammenfassung
ShellCheck und BATS sind die zwei Werkzeuge, die Shell-Skripte von "läuft meistens" zu "läuft zuverlässig" heben. ShellCheck analysiert statisch und findet die häufigsten Fehlerklassen — unquotete Variablen (SC2086), verschluckte Exit-Codes bei local (SC2155), fehlende Fehlerbehandlung nach cd (SC2164). BATS testet das tatsächliche Laufzeitverhalten: Funktionen, Exit-Codes, Ausgaben und Seiteneffekte. Mocking-Strategien isolieren externe Befehle für reproduzierbare Unit-Tests.
Die CI-Integration ist der Multiplikator: ShellCheck als obligatorischer CI-Job verhindert das Einmergen von Skripten mit bekannten Fehlermustern. BATS als paralleler Test-Job verifiziert, dass Funktionen unter definierten Bedingungen das erwartete Verhalten zeigen. Pre-Commit-Hooks mit ShellCheck erkennen Probleme noch früher. Wer beide Ebenen konsequent einsetzt, hat eine Qualitätssicherungsstrategie für Shell-Skripte, die mit professioneller Software-Entwicklung in anderen Sprachen vergleichbar ist.
ShellCheck und Tests für Bash-Skripte — Das Wichtigste auf einen Blick
ShellCheck
Statischer Analyzer für Shell-Skripte. SC2086, SC2155, SC2164 sind die häufigsten Fundstellen. .shellcheckrc für projekt-weite Konfiguration. CI-Integration als Pflicht-Job.
BATS-Framework
Unit-Tests mit @test-Blöcken, run-Helper, bats-assert für lesbare Assertions. Setup/teardown für Isolation. Für kritische Deployment-Skripte und Funktionsbibliotheken.
Mocking
Shell-Funktionen überschreiben externe Befehle. export -f für Subshells. In setup() definieren, in teardown() mit unset -f entfernen.
CI-Integration
ShellCheck + BATS als parallele Jobs. Pre-Commit-Hook für lokales ShellCheck. GitHub Actions shellcheck-action oder GitLab CI mit koalaman/shellcheck-alpine.