Externe Kommandos in Bash-Tests mocken und stubben
AI generated
$_
#!/
Bash · Mocking · Testing · bats-mock
Externe Kommandos in Bash-Tests mocken
curl, ssh und Co. kontrolliert simulieren statt echte Systeme anzufassen

Ein Bash-Skript, das curl, ssh oder mysqldump aufruft, laesst sich nicht sinnvoll testen, ohne echte Netzwerkverbindungen oder Datenbanken bei jedem Testlauf anzufassen. Externe Kommandos mocken loest genau dieses Problem: das PATH-Override-Pattern und gezielte Funktionsueberschreibung machen Bash-Tests deterministisch, schnell und unabhaengig von echter Infrastruktur.

18 Min. Lesezeit PATH-Override · Funktions-Mocks · bats-mock Bash 4.x · 5.x · Linux · macOS

1. Warum externe Kommandos in Tests zum Problem werden

Bash-Skripte sind selten in sich geschlossen. Sie rufen curl fuer HTTP-Requests, ssh fuer entfernte Befehle, mysqldump fuer Datenbank-Backups oder docker fuer Container-Operationen auf. Genau diese externen Abhaengigkeiten machen Kommandos mocken in Bash notwendig: Ohne Mocking wuerde jeder Testlauf eine echte HTTP-Verbindung aufbauen, eine echte SSH-Sitzung starten oder eine echte Datenbank anfassen, mit allen Konsequenzen fuer Geschwindigkeit, Zuverlaessigkeit und Nebenwirkungen.

Das Problem hat mehrere Dimensionen. Erstens die Geschwindigkeit: Ein echter curl-Aufruf gegen einen entfernten Server dauert Sekunden, ein gemocktes Kommando antwortet in Millisekunden. Zweitens die Zuverlaessigkeit: Ein Testlauf sollte nicht fehlschlagen, nur weil ein externer Dienst gerade nicht erreichbar ist. Drittens die Determinismus-Frage: Ein Test, der von der aktuellen Systemzeit, einer Zufallszahl oder dem tatsaechlichen Zustand einer Datenbank abhaengt, liefert bei jedem Lauf ein anderes Ergebnis, was Testautomatisierung praktisch unmoeglich macht.

Die Loesung fuer all diese Probleme ist dieselbe: externe Kommandos durch kontrollierte Attrappen ersetzen, die vorhersehbares Verhalten liefern, ohne die echte Aussenwelt anzufassen. In Bash gibt es dafuer zwei etablierte Techniken, das PATH-Override-Pattern und das direkte Ueberschreiben von Funktionsnamen, die in den naechsten Abschnitten im Detail vorgestellt werden.

2. Das PATH-Override-Pattern: Stub-Skripte vor die echten Kommandos stellen

Die grundlegende Technik zum Kommandos mocken in Bash nutzt die Reihenfolge, in der die Shell Befehle im PATH sucht. Wird ein Verzeichnis mit einem gleichnamigen Stub-Skript vor die regulaeren Systempfade gestellt, findet Bash den Stub zuerst und ruft nie das echte Kommando auf. Das funktioniert, weil Bash PATH von links nach rechts durchsucht und beim ersten Treffer stoppt, unabhaengig davon, ob spaeter im PATH noch das echte Kommando existiert.

Fuer diese Technik legt man ein Testverzeichnis mit ausfuehrbaren Dateien gleichen Namens wie die zu mockenden Kommandos an, zum Beispiel test/mocks/curl. Vor dem eigentlichen Testlauf wird dieses Verzeichnis mit PATH="test/mocks:$PATH" temporaer vorangestellt. Der grosse Vorteil dieses Ansatzes gegenueber Funktionsueberschreibung: Er funktioniert auch dann, wenn das zu testende Skript das Kommando nicht als Bash-Builtin, sondern ueber exec, in einer Subshell oder aus einem anderen Prozess heraus aufruft, da PATH systemweit fuer jeden neuen Prozess gilt.


#!/usr/bin/env bash
# test/mocks/curl — stub replacing the real curl binary
# Records every invocation and returns a canned response

echo "$*" >> "${MOCK_LOG:-/tmp/mock-curl.log}"

case "$1 $2" in
  *"api.mironsoft.de/status"*)
    echo '{"status":"ok"}'
    exit 0
    ;;
  *"api.mironsoft.de/deploy"*)
    echo '{"error":"unauthorized"}'
    exit 22
    ;;
  *)
    echo "mock curl: unhandled arguments: $*" >&2
    exit 1
    ;;
esac

#!/usr/bin/env bash
# Running the test with the mock ahead of the real curl in PATH
set -euo pipefail

readonly TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PATH="${TEST_DIR}/mocks:${PATH}"
export MOCK_LOG="$(mktemp)"

# The script under test calls "curl", but resolves to our mock first
./check_deployment_status.sh

grep -q "api.mironsoft.de/status" "$MOCK_LOG" && echo "Mock was called correctly"

Ein wichtiges Detail beim PATH-Override: Der Stub muss ausfuehrbar sein (chmod +x) und darf keine Dateiendung tragen, da das zu testende Skript den Befehl als curl aufruft, nicht als curl.sh. Ausserdem sollte PATH nach dem Testlauf zuverlaessig zurueckgesetzt werden, am besten ueber eine Subshell oder einen trap, damit nachfolgende Tests nicht versehentlich denselben Mock verwenden.

3. Funktionen ueberschreiben statt PATH manipulieren

Eine leichtgewichtigere Alternative zum PATH-Override ist das direkte Ueberschreiben von Funktionsnamen innerhalb der Testumgebung. Definiert man vor dem Sourcen des zu testenden Skripts eine Bash-Funktion mit demselben Namen wie ein externes Kommando, etwa curl(), ruft Bash bei jedem Aufruf von curl innerhalb desselben Shell-Prozesses diese Funktion auf statt des echten Binaries. Funktionen haben in Bash Vorrang vor Kommandos im PATH, solange kein vollqualifizierter Pfad wie /usr/bin/curl verwendet wird.

Dieser Ansatz zum Kommandos mocken ist besonders praktisch fuer schnelle, isolierte Unit-Tests einzelner Funktionen, da kein separates Skript-File und keine PATH-Manipulation noetig sind, alles findet innerhalb derselben Testdatei statt. Der Nachteil: Funktions-Mocks wirken nur innerhalb desselben Bash-Prozesses. Ruft das zu testende Skript ein externes Kommando aus einer Subshell mit $(...) auf, in der die Funktion nicht exportiert wurde, greift der Mock unter Umstaenden nicht.


#!/usr/bin/env bats
load "test_helper/bats-assert/load"

setup() {
  load "../lib/deployment.sh"

  # Override curl with a mock function for this test file
  curl() {
    echo "MOCK: curl called with args: $*" >&2
    if [[ "$*" == *"api.mironsoft.de/status"* ]]; then
      echo '{"status":"ok"}'
      return 0
    fi
    return 1
  }
  export -f curl
}

@test "check_deployment_status parses mocked curl response correctly" {
  run check_deployment_status
  assert_success
  assert_output --partial "Deployment is healthy"
}

@test "check_deployment_status handles curl failure gracefully" {
  curl() { return 7; }
  export -f curl

  run check_deployment_status
  assert_failure
  assert_output --partial "Could not reach API"
}

Der Befehl export -f ist entscheidend: Ohne ihn ist die Funktion nur im aktuellen Shell-Kontext sichtbar, nicht aber in Subshells, die durch Kommando-Substitution oder Hintergrundprozesse entstehen. Wird eine Funktion mit export -f exportiert, erben auch Subshells und aus dem Skript gestartete Kindprozesse die Ueberschreibung, solange es sich um Bash-Kindprozesse handelt, nicht um vollstaendig neue Programme, die den PATH selbst neu aufloesen.

4. Aufrufe protokollieren und Argumente pruefen

Ein Mock, der nur eine feste Antwort liefert, testet lediglich, ob das Skript mit einer bestimmten Antwort umgehen kann. Genauso wichtig ist oft die umgekehrte Pruefung: Wurde das externe Kommando ueberhaupt mit den richtigen Argumenten aufgerufen? Diese Pruefung erfordert, dass der Mock jeden Aufruf protokolliert, typischerweise in eine Logdatei, die der Test anschliessend auswertet.

Ein solides Protokollierungs-Pattern schreibt bei jedem Mock-Aufruf eine Zeile mit allen Argumenten in eine Datei, deren Pfad ueber eine Umgebungsvariable konfigurierbar ist. Der Test kann danach mit grep oder direktem Zeilenvergleich pruefen, ob das externe Kommando mit den erwarteten Parametern aufgerufen wurde, und sogar die Reihenfolge mehrerer Aufrufe verifizieren, etwa dass zuerst ein Backup erstellt und erst danach die Datenbank geleert wurde.


#!/usr/bin/env bats
load "test_helper/bats-assert/load"

setup() {
  load "../lib/backup.sh"
  CALL_LOG="$(mktemp)"

  mysqldump() {
    echo "mysqldump $*" >> "$CALL_LOG"
    echo "-- mock dump output"
  }
  export -f mysqldump
  export CALL_LOG
}

teardown() {
  rm -f "$CALL_LOG"
}

@test "backup_database calls mysqldump with correct database name" {
  run backup_database "shop_production"
  assert_success
  run grep -c "mysqldump.*shop_production" "$CALL_LOG"
  assert_output "1"
}

@test "backup_database calls mysqldump exactly once" {
  run backup_database "shop_production"
  run wc -l < "$CALL_LOG"
  assert_output "1"
}

@test "backup_database passes the --single-transaction flag" {
  run backup_database "shop_production"
  run grep -c -- "--single-transaction" "$CALL_LOG"
  assert_output "1"
}

Dieses Muster verwandelt einen einfachen Mock in einen sogenannten Spy: Er simuliert nicht nur das externe Verhalten, sondern zeichnet auch auf, wie er verwendet wurde. Diese Aufzeichnung ist besonders wertvoll bei Refactorings: Wenn ein Test pruefen soll, dass mysqldump weiterhin mit denselben kritischen Flags aufgerufen wird, faellt eine versehentliche Aenderung dieser Flags sofort auf, ohne dass die eigentliche Datenbankfunktion angefasst werden muesste.

5. Rueckgabewerte und Exit-Codes von Mocks steuern

Die eigentliche Staerke von Mocks liegt darin, Fehlerszenarien zu simulieren, die mit einem echten Kommando kaum reproduzierbar waeren: ein Netzwerktimeout, ein abgelaufenes Zertifikat, ein voller Datenspeicher oder ein temporaerer Serverfehler mit HTTP-Status 503. Ein Mock, der bei Bedarf einen bestimmten Exit-Code oder eine bestimmte Fehlerausgabe zurueckgibt, macht solche Szenarien in Sekunden reproduzierbar testbar, ohne die entsprechende Fehlerbedingung in einer echten Umgebung erst kuenstlich herbeifuehren zu muessen.

Um verschiedene Szenarien in derselben Testsuite abzudecken, konfiguriert man den Mock ueber eine Umgebungsvariable, die das gewuenschte Verhalten steuert. So kann ein und derselbe Mock je nach Testfall Erfolg, einen bestimmten Fehlercode oder eine verzoegerte Antwort simulieren, ohne fuer jedes Szenario ein eigenes Stub-Skript pflegen zu muessen.


#!/usr/bin/env bash
# test/mocks/curl — mock with configurable failure modes via env var

echo "$*" >> "${MOCK_LOG:-/dev/null}"

case "${MOCK_CURL_BEHAVIOR:-success}" in
  success)
    echo '{"status":"ok"}'
    exit 0
    ;;
  timeout)
    echo "curl: (28) Operation timed out" >&2
    exit 28
    ;;
  unauthorized)
    echo '{"error":"invalid token"}'
    exit 22
    ;;
  server_error)
    echo '{"error":"internal server error"}'
    exit 22
    ;;
  *)
    echo "Unknown MOCK_CURL_BEHAVIOR: ${MOCK_CURL_BEHAVIOR}" >&2
    exit 1
    ;;
esac

#!/usr/bin/env bats
load "test_helper/bats-assert/load"

setup() {
  export PATH="$(pwd)/test/mocks:${PATH}"
}

@test "deploy retries on timeout and eventually fails" {
  export MOCK_CURL_BEHAVIOR="timeout"
  run deploy_to_production
  assert_failure
  assert_output --partial "Deployment failed after 3 retries"
}

@test "deploy fails fast on unauthorized without retrying" {
  export MOCK_CURL_BEHAVIOR="unauthorized"
  run deploy_to_production
  assert_failure
  assert_output --partial "Authentication failed"
  refute_output --partial "retry"
}

Dieses Muster zeigt einen zentralen Vorteil von Mocking gegenueber echten Integrationstests: Man kann gezielt pruefen, ob die Retry-Logik bei einem Timeout tatsaechlich mehrfach versucht, waehrend sie bei einem Autorisierungsfehler sofort abbricht, ohne unnoetige Versuche zu verschwenden. Diese Unterscheidung im Fehlerverhalten waere mit einem echten Server kaum zuverlaessig zu erzwingen.

6. Mocking mit bats-mock strukturieren

Fuer groessere Testsuiten mit vielen gemockten Kommandos wird das manuelle Schreiben von Stub-Skripten schnell repetitiv. Die Bibliothek bats-mock baut auf dem PATH-Override-Pattern auf, kapselt aber die Erstellung, Konfiguration und Auswertung von Mocks in wiederverwendbaren Helper-Funktionen. Statt jeden Mock manuell als Datei anzulegen, generiert bats-mock Stub-Skripte zur Laufzeit und bietet Assertions wie assert_called_with, um Aufrufe direkt zu pruefen.

Der Vorteil von bats-mock gegenueber selbstgeschriebenen Stub-Skripten liegt in der Konsistenz ueber eine wachsende Testsuite hinweg: Jeder Mock folgt derselben Struktur, jede Assertion nutzt dieselbe Syntax, und neue Teammitglieder muessen kein projektspezifisches Mocking-Pattern erst verstehen, sondern koennen sich auf eine dokumentierte, etablierte Bibliothek verlassen.


#!/usr/bin/env bats
load "test_helper/bats-mock/load"
load "test_helper/bats-assert/load"

setup() {
  load "../lib/backup.sh"
  mock_aws="$(mock_create)"
  # Redirect calls to "aws" toward our generated mock stub
  ln -sf "$mock_aws" "$(dirname "$mock_aws")/aws"
  export PATH="$(dirname "$mock_aws"):${PATH}"
}

@test "upload_backup calls aws s3 cp with the correct bucket path" {
  mock_set_output "$mock_aws" "upload: ok" 0

  run upload_backup "/tmp/backup.sql.gz" "production-backups"

  assert_success
  assert_equal "$(mock_get_call_num "$mock_aws")" 1
  assert_equal "$(mock_get_call_args "$mock_aws" 1)" \
    "s3 cp /tmp/backup.sql.gz s3://production-backups/"
}

@test "upload_backup surfaces an aws error correctly" {
  mock_set_output "$mock_aws" "Access Denied" 1

  run upload_backup "/tmp/backup.sql.gz" "production-backups"

  assert_failure
  assert_output --partial "Access Denied"
}

Ein Detail, das bats-mock von manuellen Stub-Skripten unterscheidet: Die generierten Mocks zeichnen automatisch die Anzahl der Aufrufe und alle Argumente in strukturierter Form auf, sodass Assertions wie mock_get_call_args direkt auf bestimmte Aufrufe zugreifen koennen, ohne selbst geschriebene Log-Parsing-Logik. Fuer Projekte mit mehr als einer Handvoll gemockter Kommandos amortisiert sich dieser initiale Lernaufwand schnell.

7. Zeit, Zufall und Netzwerk deterministisch machen

Neben externen Kommandos gibt es weitere Quellen von Nichtdeterminismus, die Bash-Tests unzuverlaessig machen: die aktuelle Systemzeit ueber date, Zufallszahlen ueber $RANDOM oder /dev/urandom, und generierte eindeutige IDs ueber uuidgen oder mktemp. Fuer verlaessliche Tests muessen auch diese Quellen kontrolliert werden, sonst erzeugt jeder Testlauf leicht unterschiedliche Ergebnisse, was Assertions auf exakte Werte unmoeglich macht.

Das Bash-Pattern dafuer ist identisch zum Mocken externer Kommandos: date() als Funktion ueberschreiben, die immer denselben festen Zeitstempel zurueckgibt, oder $RANDOM in Testumgebungen durch einen festen Seed reproduzierbar machen. Fuer Netzwerkoperationen, die nicht ueber ein einzelnes Kommando wie curl laufen, sondern direkt ueber TCP-Sockets, hilft ein lokaler Test-Server, der auf einem festen Port simulierte Antworten liefert, statt gegen einen echten externen Dienst zu testen.


#!/usr/bin/env bats
load "test_helper/bats-assert/load"

setup() {
  load "../lib/release.sh"

  # Freeze time for deterministic release tag generation
  date() {
    echo "2026-07-30T14:00:00Z"
  }
  export -f date

  # Deterministic "random" ID for reproducible test assertions
  generate_release_id() {
    echo "test-fixed-id-0001"
  }
  export -f generate_release_id
}

@test "create_release_tag produces a deterministic, predictable tag" {
  run create_release_tag
  assert_success
  assert_output "release-2026-07-30-test-fixed-id-0001"
}

Diese Technik hat einen weiteren Vorteil: Tests, die auf einer festen Zeit basieren, decken auch Randfaelle rund um Zeitzonen und Datumsgrenzen zuverlaessig ab, etwa das Verhalten eines Skripts am 31. Dezember um 23:59 Uhr, ein Fall, der ohne kontrollierte Zeit nur einmal im Jahr zufaellig getestet wuerde, waehrend er mit einem gemockten date jederzeit reproduzierbar bleibt.

8. Grenzen des Mockens: wann ein Integrationstest noetig ist

So nuetzlich Mocks sind, sie testen niemals das tatsaechliche Verhalten des echten externen Kommandos, sondern nur die Annahmen des Testautors darueber. Wenn sich das Ausgabeformat von curl, das Verhalten einer API oder die Argumentstruktur eines Tools wie aws aendert, bleibt ein Mock-Test weiterhin gruen, obwohl das echte Skript in Produktion bricht. Dieses Risiko wird als "Mock-Drift" bezeichnet: Die Attrappe entfernt sich schleichend vom echten Verhalten, ohne dass es jemand bemerkt.

Deshalb sollten gemockte Kommandos niemals die einzige Testebene bleiben. Eine kleine Anzahl von Integrationstests, die tatsaechlich gegen eine Staging-Umgebung, einen echten Test-Server oder eine lokale Docker-Instanz laufen, deckt genau die Faelle ab, die ein Mock per Definition nicht pruefen kann: ob die Annahmen ueber das externe Verhalten tatsaechlich noch stimmen. Die Faustregel: viele schnelle Mock-Tests fuer die Logik des eigenen Skripts, wenige, aber regelmaessig laufende Integrationstests fuer die Kompatibilitaet mit der echten Aussenwelt.

9. Mocking-Techniken im Vergleich

Je nach Anwendungsfall eignen sich unterschiedliche Mocking-Techniken besser oder schlechter, abhaengig von der Testgeschwindigkeit, dem Setup-Aufwand und der Genauigkeit der Simulation.

Technik Setup-Aufwand Wirkungsbereich Am besten geeignet fuer
Funktion ueberschreiben Sehr gering Nur aktueller Shell-Prozess Schnelle Unit-Tests einzelner Funktionen
PATH-Override Mittel Auch Subshells und Kindprozesse Skripte mit exec oder externen Prozessen
bats-mock Mittel (einmalig) Auch Subshells und Kindprozesse Grosse Testsuiten mit vielen Mocks
Lokaler Test-Server Hoch Realistisches Netzwerkverhalten HTTP-APIs mit komplexem Verhalten
Echter Integrationstest Hoch, langsam Volle Realitaet Regelmaessige Kompatibilitaetspruefung

In der Praxis kombiniert man diese Techniken je nach Testebene: Funktions-Mocks fuer schnelle, isolierte Unit-Tests, PATH-Override oder bats-mock fuer realistischere Simulation ganzer Skriptablaeufe, und eine kleine Anzahl echter Integrationstests, die regelmaessig, aber seltener laufen, um Mock-Drift rechtzeitig zu erkennen, bevor er in Produktion sichtbar wird.

Mironsoft

Shell-Automatisierung, Testing und Deployment-Infrastruktur

Bash-Skripte mit externen Abhaengigkeiten zuverlaessig testen?

Wir bauen Mocking-Strategien fuer eure Bash-Skripte auf, von einfachen Funktions-Mocks bis zu strukturierten bats-mock-Testsuiten, ergaenzt um gezielte Integrationstests gegen echte Systeme.

Mocking-Setup

PATH-Override und Funktions-Mocks fuer curl, ssh, aws und mehr

Testsuite-Ausbau

bats-mock-Integration fuer wachsende, wartbare Bash-Testsuiten

Mock-Drift-Pruefung

Ergaenzende Integrationstests gegen echte Staging-Systeme

10. Zusammenfassung

Externe Kommandos mocken ist die Grundvoraussetzung, um Bash-Skripte mit Abhaengigkeiten zu curl, ssh, aws oder Datenbank-Tools schnell und deterministisch zu testen. Das PATH-Override-Pattern stellt Stub-Skripte vor die echten Kommandos, funktioniert auch in Subshells und Kindprozessen und ist damit robuster als reine Funktionsueberschreibung, die nur im aktuellen Shell-Prozess wirkt. Aufrufprotokollierung verwandelt einfache Mocks in Spies, die pruefen koennen, ob und wie ein externes Kommando aufgerufen wurde.

bats-mock strukturiert das Mocking fuer wachsende Testsuiten, waehrend die kontrollierte Simulation von Zeit und Zufall zusaetzlichen Nichtdeterminismus eliminiert. Wichtig bleibt, dass gemockte Kommandos niemals die einzige Testebene sind: Eine kleine Anzahl echter Integrationstests gegen tatsaechliche Systeme faengt Mock-Drift ab, bevor veraltete Annahmen ueber externes Verhalten in Produktion zu echten Fehlern fuehren.

Externe Kommandos in Bash-Tests mocken — Das Wichtigste auf einen Blick

PATH-Override

Stub-Skript vor die echten Systempfade stellen, funktioniert auch in Subshells und Kindprozessen.

Funktions-Mocks

Schnell und leichtgewichtig fuer isolierte Unit-Tests, mit export -f fuer Subshell-Sichtbarkeit.

Aufrufprotokollierung

Mock als Spy erweitern, der Argumente und Anzahl der Aufrufe fuer Assertions aufzeichnet.

Grenzen kennen

Mock-Drift durch regelmaessige echte Integrationstests gegen tatsaechliche Systeme abfangen.

11. FAQ: Externe Kommandos in Bash-Tests mocken

1Warum externe Kommandos mocken?
Echte Aufrufe machen Tests langsam, unzuverlaessig und infrastrukturabhaengig. Mocking loest das.
2Wie funktioniert PATH-Override?
Stub-Verzeichnis wird vor die echten Systempfade in PATH gestellt, Bash findet den Stub zuerst.
3Funktion oder PATH ueberschreiben?
Funktion fuer schnelle Unit-Tests im selben Prozess, PATH-Override fuer Subshells und Kindprozesse.
4Wozu export -f?
Macht die Mock-Funktion auch in Subshells und Bash-Kindprozessen sichtbar.
5Aufruf-Argumente pruefen?
Mock protokolliert Aufrufe in eine Logdatei, Test wertet sie mit grep aus.
6Verschiedene Fehlerszenarien simulieren?
Ueber Umgebungsvariable, die im Mock das Verhalten steuert.
7Vorteil von bats-mock?
Generierte Mocks und einheitliche Assertions statt selbstgeschriebener Stub-Logik.
8Zeit und Zufall deterministisch machen?
date und aehnliche Funktionen ueberschreiben, feste Werte zurueckgeben.
9Was ist Mock-Drift?
Mock veraltet gegenueber echtem Verhalten, Test bleibt gruen, echtes Skript bricht.
10Mocking komplett ersetzen?
Nein, wenige echte Integrationstests ergaenzend gegen Mock-Drift einsetzen.