Bash-Testen
Test-Frameworks und -Muster für Shell-Skripte, mit Fokus auf shunit2 und shellspec.
Framework-Auswahl
| Framework | Stärken | Am besten geeignet für |
|---|---|---|
| shunit2 | xUnit-Stil, einfach, portabel | Unit-Tests, Funktions-Tests |
| shellspec | BDD-Stil, modern, umfangreich | Verhaltensspezifikationen, ganze Projekte |
shunit2
Installation
# Direkter Download
curl -L https://github.com/kward/shunit2/raw/master/shunit2 -o shunit2
# Oder über Paketmanager
apt install shunit2
brew install shunit2
Grundlegende Teststruktur
#!/usr/bin/env bash
# Das zu testende Skript einbinden
source ./my_script.sh
# Testfunktionen beginnen mit test
test_addition() {
result=$(add 2 3)
assertEquals "2 + 3 sollte 5 ergeben" "5" "$result"
}
test_string_output() {
result=$(greet "World")
assertEquals "Hallo, Welt" "$result"
}
test_file_creation() {
create_temp_file
assertTrue "Temp-Datei sollte existieren" "[ -f /tmp/testfile ]"
}
test_exit_code() {
validate_input "valid"
assertEquals "Sollte 0 für gültige Eingabe zurückgeben" 0 $?
}
# Setup und Teardown
oneTimeSetUp() {
# Wird einmal vor allen Tests ausgeführt
export TEST_DIR=$(mktemp -d)
}
oneTimeTearDown() {
# Wird einmal nach allen Tests ausgeführt
rm -rf "$TEST_DIR"
}
setUp() {
# Wird vor jedem Test ausgeführt
cd "$TEST_DIR"
}
tearDown() {
# Wird nach jedem Test ausgeführt
rm -f "$TEST_DIR"/*
}
# shunit2 einbinden
source shunit2
shunit2-Assertionen
# Gleichheit
assertEquals [Nachricht] erwartet aktuell
assertNotEquals [Nachricht] unerwartet aktuell
# Null/Leer
assertNull [Nachricht] wert
assertNotNull [Nachricht] wert
# Boolesch/Status
assertTrue [Nachricht] bedingung
assertFalse [Nachricht] bedingung
# Gleiche Referenz (String-Vergleich)
assertSame [Nachricht] erwartet aktuell
assertNotSame [Nachricht] unerwartet aktuell
# Enthält (im String)
assertContains [Nachricht] container inhalt
# Exit-Status
assertEquals 0 $?
Funktionen isoliert testen
#!/usr/bin/env bash
# test_my_functions.sh
source ./my_functions.sh
test_calculate_sum_empty() {
result=$(calculate_sum)
assertEquals "Leere Summe sollte 0 sein" "0" "$result"
}
test_calculate_sum_single() {
result=$(calculate_sum 5)
assertEquals "5" "$result"
}
test_calculate_sum_multiple() {
result=$(calculate_sum 1 2 3 4 5)
assertEquals "15" "$result"
}
test_validate_email_valid() {
validate_email "[email protected]"
assertTrue "Gültige E-Mail sollte bestehen" $?
}
test_validate_email_invalid() {
validate_email "not-an-email"
assertFalse "Ungültige E-Mail sollte fehlschlagen" $?
}
source shunit2
shellspec
Installation
# Über curl
curl -fsSL https://git.io/shellspec | sh
# Über Homebrew
brew install shellspec
# Über Paketmanager
apt install shellspec
Verzeichnisstruktur
project/
├── lib/
│ └── functions.sh
├── spec/
│ ├── spec_helper.sh
│ ├── functions_spec.sh
│ └── support/
│ └── fixtures/
└── .shellspec
Konfiguration (.shellspec)
--require spec_helper
--format documentation
--color
Grundlegende Spec-Struktur
shellspec-Matcher
# Ausgabe-Matcher
The output should eq "exakte Übereinstimmung"
The output should include "teilweise"
The output should start with "präfix"
The output should end with "suffix"
The output should match pattern "*glob*"
The output should be blank
# Status-Matcher
The status should be success # exit 0
The status should be failure # exit ungleich 0
The status should eq 1 # spezifischer Code
# Variablen-Matcher
The variable VAR should eq "wert"
The variable VAR should be defined
The variable VAR should be undefined
# Datei-Matcher
The file "pfad" should be exist
The file "pfad" should be file
The file "pfad" should be directory
The file "pfad" should be readable
# Pfad-Matcher
The path "datei.txt" should be exist
Mocking in shellspec
Describe 'deploy-Funktion'
# Externen Befehl mocken
curl() {
echo "gemockte Antwort"
return 0
}
It 'ruft API-Endpunkt auf'
When call deploy "server"
The output should include "gemockte Antwort"
End
End
Describe 'Dateioperationen'
# Mocken durch Überschreiben der Funktion
Mock rm
echo "rm aufgerufen mit: $*"
End
It 'versucht Datei zu löschen'
When call cleanup_temp
The output should include "rm aufgerufen mit"
End
End
Spec Helper
# spec/spec_helper.sh
# Gemeinsame Funktionen laden
spec_helper_precheck() {
minimum_version "0.28.0"
}
spec_helper_loaded() {
# Testumgebung einrichten
export TEST_MODE=true
}
spec_helper_configure() {
# Projektfunktionen importieren
import 'lib/functions.sh'
}
Testmuster
Exit-Codes testen
# shunit2
test_success_exit() {
run_command "gültige_eingabe"
assertEquals 0 $?
}
test_error_exit() {
run_command "ungültige_eingabe"
assertEquals 1 $?
}
# shellspec
It 'beendet mit 0 bei Erfolg'
When call run_command "gültige_eingabe"
The status should eq 0
End
stdout und stderr testen
# shunit2
test_stdout() {
result=$(my_function 2>/dev/null)
assertEquals "erwartete Ausgabe" "$result"
}
test_stderr() {
error=$(my_function 2>&1 >/dev/null)
assertContains "$error" "Fehlermeldung"
}
# shellspec
It 'gibt auf stdout aus'
When call my_function
The stdout should eq "erwartete Ausgabe"
End
It 'gibt Fehler auf stderr aus'
When call my_function
The stderr should include "Fehlermeldung"
End
Mit Fixtures testen
# Test-Fixtures einrichten
setUp() {
TEST_DIR=$(mktemp -d)
cat > "$TEST_DIR/config.json" <<EOF
{
"key": "value"
}
EOF
}
tearDown() {
rm -rf "$TEST_DIR"
}
test_config_parsing() {
result=$(parse_config "$TEST_DIR/config.json")
assertEquals "value" "$result"
}
Interaktive Funktionen testen
# Eingabe über heredoc bereitstellen
test_interactive() {
result=$(my_prompt <<EOF
ja
EOF
)
assertEquals "bestätigt" "$result"
}
# Oder printf verwenden
test_with_input() {
result=$(printf 'yes\n' | my_prompt)
assertEquals "bestätigt" "$result"
}
Tests ausführen
# shunit2
./test_script.sh
# shellspec
shellspec # Alle Specs ausführen
shellspec spec/file_spec.sh # Bestimmte Spec ausführen
shellspec --format tap # TAP-Ausgabe
shellspec --jobs 4 # Parallele Ausführung
Best Practices
- Eine Assertion pro Test wenn praktikabel
- Aussagekräftige Testnamen die erklären, was getestet wird
- Tests isolieren – keine Abhängigkeiten zwischen Tests
- Grenzfälle testen – leere Eingabe, Sonderzeichen, große Datenmengen
- Ressourcen bereinigen in tearDown
- Externe Befehle mocken – nicht curl testen, sondern die eigene Logik
- Exit-Codes testen nicht nur die Ausgabe