ShellCheck · BATS · Testing · CI/CD · Shell
ShellCheck und Tests für Bash-Skripte
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.

13 Min. Lesezeit ShellCheck · BATS · Mocking · Coverage · CI/CD Bash 4.x · 5.x · GitHub Actions · GitLab CI

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.shShellCheck 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.

11. FAQ: ShellCheck und Tests für Bash-Skripte

1Was ist ShellCheck und wie installiere ich es?
Statischer Analyzer für Shell-Skripte. apt install shellcheck oder brew install shellcheck. Verwendung: shellcheck skript.sh — gibt kategorisierte Warnungen mit Doku-Links aus.
2BATS vs. ShellCheck — wo ist der Unterschied?
ShellCheck = statische Analyse. BATS = Laufzeittests. Beide ergänzen sich — ShellCheck findet Strukturfehler, BATS verifiziert tatsächliches Verhalten.
3Externe Befehle in BATS mocken?
Shell-Funktion mit identischem Namen definieren, export -f für Subshells. In teardown() mit unset -f entfernen um andere Tests nicht zu beeinflussen.
4Welche ShellCheck-Regel ist am wichtigsten?
SC2086 (unquoted variable) am häufigsten. SC2155 (local mit Command-Substitution) am gefährlichsten — verschluckt Exit-Codes. SC2164 (cd ohne Fehlerbehandlung) in Deployment-Skripten kritisch.
5ShellCheck in GitHub Actions integrieren?
uses: ludeeus/action-shellcheck@master mit severity: warning. Alle .sh-Dateien werden automatisch geprüft — kein weiteres Setup nötig.
6ShellCheck-Warnungen inline deaktivieren?
# shellcheck disable=SC2086 vor der betroffenen Zeile. Projektweit in .shellcheckrc. Sparsam einsetzen und immer begründen.
7BATS-Tests strukturieren für Deployment-Skripte?
Funktionen per source einbinden. Setup: tmp-Verzeichnis + Umgebungsvariablen. Teardown: aufräumen + Mocks entfernen. Eine Assertion pro Test, Fehlerfälle explizit testen.
8Was bringt bats-assert?
Lesbare Assertions: assert_success, assert_failure, assert_output, assert_output --regexp, assert_line. Fehler zeigen konkrete Ausgabe und erwarteten Wert.
9Coverage für Bash-Skripte messen?
bashcov als dediziertes Tool. Pragmatisch: kritische Pfade als Checkliste definieren und für jeden einen BATS-Test schreiben.
10Lohnt sich BATS für kleine Skripte?
Für Wrapper-Skripte reicht ShellCheck. BATS lohnt sich bei Entscheidungslogik, kritischen Ressourcen (Deployments, Backups) oder Mehrfach-Umgebungs-Betrieb.