Dass ein Probelauf einmal erfolgreich war, heißt noch nicht, dass auf das getestete Skript in jeder Situation Verlass ist. Um das zu garantieren, gilt es viel mehr Testfälle zu überprüfen. Dabei hilft das Framework Shell-Spec.
In der vorherigen Folge dieser Serie haben wir ein kleines Framework geschrieben, mit dem wir unsere Skripte in Module aufteilen können. Für einen Test haben wir außerdem ein Modul mit Funktionen zur Behandlung von Arrays implementiert, das wir kurzerhand in einem Beispielskript verwendet haben. Das Beispielskript hat wie erwartet funktioniert, weshalb wir annehmen, dass unser Modul fehlerlos ist.
Ob eine Funktion aber wirklich alle Eingaben inklusive der Grenzfälle korrekt behandelt, lässt sich nach ein oder zwei Aufrufen noch nicht beurteilen. Um Vertrauen in unsere Funktionen zu schaffen, müssen wir sie mit möglichst vielen verschiedenen Eingaben testen und die Ausgaben auf Richtigkeit prüfen. Damit wir aber nicht Hunderte von Tests händisch schreiben müssen, gibt es Frameworks, die uns bei sogenannten Unit-Tests die Arbeit erleichtern. In diesem Beitrag sehen wir uns Shell-Spec [1] genauer an, den Neuankömmling unter den Test-Frameworks für Shell-Skripte.
Vorbereitung
Bevor wir loslegen können, müssen wir Shell-Spec erst einmal installieren. Obwohl es mittlerweile seit fast drei Jahren entwickelt wird, stellt Arch Linux bisher als einzige Distribution Pakete für Shell-Spec bereit. Sie müssen es daher aus den Quellen installieren. Da Shell-Spec außer einer POSIX-konformen Shell keine Abhängigkeiten hat, können Sie es direkt von Github herunterladen und mittels des beiliegenden Makefiles installieren (Listing 1).
Listing 1
Shell-Spec installieren
$ git clone https://github.com/shellspec/shellspec $ cd shellspec $ sudo make install
Tests einrichten
Nun geht es daran, Tests für das Array-Modul aus der letzten Folge zu definieren. Dazu müssen Sie ein Specfile schreiben – so heißen bei Shell-Spec die Dateien, die die Testcases beschreiben. Ein Specfile umfasst eine oder mehrere Testgruppen, die wiederum einen oder mehrere Testcases enthalten.
Eine Testgruppe definieren Sie mit dem Schlüsselwort »Describe«, gefolgt vom Namen der Testgruppe. Der Name der Testgruppe hat keine Relevanz für Shell-Spec, hilft aber dabei, die Ausgaben zuzuordnen. Am besten verwenden Sie den Namen der zu testenden Funktion als Gruppennamen. Testcases definieren Sie mit dem Schlüsselwort »It« und einem zugehörigen Namen. Auch der dient ausschließlich der Zuordnung der Ausgaben, weshalb Sie idealerweise die Eigenschaft oder das Verhalten, die der Testcase prüft, als Name verwenden. Sowohl Testgruppen als auch Testcases schließen Sie mit dem Schlüsselwort »End« ab. Ein kurzes Beispiel für die Struktur eines Specfiles zeigt Listing 2.
Listing 2
Beispiel-Specfile
Describe "ZuTestendeFunktion"
It "ZuTestendesVerhalten"
# Testcode
End
End
Bevor Sie sich nun dem Testcode widmen, müssen Sie sich vorab Gedanken machen, welche Eigenschaften der in der letzten Folge definierten Array-Funktionen (Listing 3) Sie eigentlich testen wollen.
Listing 3
Zu testende Array-Funktionen
array_to_lines() {
local array=("$@")
if (( ${#array[@]} > 0 )); then
printf "%s\n" "${array[@]}"
fi
}
array_sort() {
local array=("$@")
array_to_lines "${array[@]}" | sort -V
}
array_contains() {
local needle="$1"
local haystack=("${@:2}")
local cur
for cur in "${haystack[@]}"; do
if [[ "$needle" == "$cur" ]]; then
return 0
fi
done
return 1
}
Die Funktion »array_to_lines()« gibt ein Array zeilenweise aus. Das erste Kriterium ist daher, dass alle Elemente des Eingabe-Arrays auch in der Ausgabe enthalten sein sollten. Zusätzlich gilt es sicherzustellen, dass Ein- und Ausgabe dieselbe Anzahl von Elementen enthalten. Zusammen mit dem ersten Kriterium stellt das sicher, dass jedes Element genau einmal ausgegeben wird. Da die Funktion leere Arrays als Sonderfall behandelt, soll ein gesonderter Testcase sicherstellen, dass die Funktion nicht auf die Standardausgabe schreibt, wenn das Eingabearray leer ist. Zuletzt sollten Sie noch einen Aspekt prüfen, auf den wir beim letzten Mal nicht eingegangen sind: die Reihenfolge der Ausgabe. Intuitiv sollte die Ausgabe in derselben Reihenfolge wie die Eingabe erfolgen. Tatsächlich reicht die Funktion die Daten jedoch nur an Printf weiter und vertraut darauf, dass das Richtige dabei herauskommt. Wir brauchen deshalb einen Test, der prüft, ob die Reihenfolge der Daten beibehalten wird.
Die Funktion »array_sort()« sortiert ein Array mithilfe des Sort-Befehls. Wie schon »array_to_lines« soll diese Funktion nichts ausgeben, wenn das Eingabearray leer ist. Anderenfalls soll sie die Elemente sortiert ausgegeben. Dabei darf keine zeichenweise Sortierung zum Einsatz kommen, da diese beispielsweise die Zahlenfolge 1, 3, 20 in 1, 20, 3 verkehren würde. Deshalb erfolgt der Aufruf von Sort mit dem Schalter »-V«. Ob daraus das gewünschte Ergebnis resultiert, gilt es per Testcase zu prüfen. Darüber hinaus müssen auch bei »array_sort()« alle Elemente aus der Eingabe in der Ausgabe enthalten sein.
Die Funktion »array_contains()« stellt fest, ob ein Array ein bestimmtes Element enthält. Häufig entsteht bei Arrays das Problem, dass der Anwender die Grenzen für Indizes falsch wählt und deshalb das erste oder letzte Element nicht korrekt behandelt wird. Um derartige Fehler zu vermeiden, prüfen drei Testcases, ob ein Element am Anfang, in der Mitte oder am Ende eines Arrays gefunden wird. Umgekehrt könnte es aber sein, dass die Funktion aufgrund eines Fehlers immer ein positives Ergebnis zurückgibt. Daher ist auch ein Testcase vonnöten, der sicherstellt, dass die Funktion keinen Erfolg meldet, wenn ein Element nicht im Array enthalten ist.
Die Funktion »array_contains()« benutzt keine Indizes. Indem Sie die Testcases aber gerade nicht auf die Implementierung zuschneiden, stellen Sie sicher, Probleme auch dann zu erkennen, wenn sich die Implementierung ändert. Zuletzt gibt es noch einen Grenzfall, der zu testen ist: Ist das an »array_contains()« übergebene Array leer, soll der Rückgabewert der Funktion nicht 0 sein.
Aus diesen Überlegungen heraus erzeugen Sie jetzt die Grundstruktur des Specfiles. Dazu definieren Sie eine Testgruppe pro Funktion und fügen für jede der oben genannten Eigenschaften einen leeren Testcase ein. Sinnigerweise bezeichnen Sie die Testcases nicht mir kryptischen Abkürzungen als Namen, sondern benutzen, wie in Listing 4 gezeigt, natürliche Sprache. Das macht es hinterher einfacher, Testergebnisse zu interpretieren. Da die Testgruppen nicht vollständig sind, verwenden Sie außerdem die »Pending«-Direktive, um Shell-Spec anzuweisen, die Tests vorläufig zu überspringen. Speichern Sie das Specfile nun als »listing4_spec.sh«, um es später mit dem Befehl »shellspec listing4_spec.sh« ausführen zu können.
Listing 4
Schema eines Specfiles
Describe "array_to_lines()" Pending "Nicht implementiert" It "gibt jedes Element aus dem Eingabearray aus" End It "gibt so viele Elemente aus, wie das Eingabearray lang ist" End It "gibt nichts aus, wenn das Eingabearray leer ist" End It "aendert nicht die Reihenfolge der Elemente" End End Describe "array_sort()" Pending "Nicht implementiert" It "gibt nichts aus, wenn das Eingabearray leer ist" End It "gibt jedes Element aus dem Eingabearray aus" End It "sortiert nicht zeichenweise" End End Describe "array_contains()" Pending "Nicht implementiert" It "findet ein Element am Anfang des Arrays" End It "findet ein Element in der Mitte des Arrays" End It "findet ein Element am Ende des Arrays" End It "gibt 1 zurueck, wenn ein Element nicht im Eingabearray ist" End It "gibt 1 zurueck, wenn das Array leer ist" End End
Zu Anfang der Sonderfall
Nachdem nun die Struktur feststeht, implementieren Sie die Testcases der Reihe nach. Schon der erste Testcase für »array_to_lines()« entpuppt sich dabei als Herausforderung: Der Vergleich eines Arrays beliebiger Länge mit der Standardausgabe einer Funktion lässt sich mit den Bordmitteln von Shell-Spec nicht realisieren. Da die Sprache von Shell-Spec aber Shell-kompatibel ist, können Sie innerhalb eines Testcases Shell-Funktionen definieren.
Sie schreiben deshalb eine Hilfsfunktion, die den Test von Hand implementiert und bei Erfolg 0 beziehungsweise im Fehlerfall 1 zurückgibt. Um die Eingabe bequem mit der Ausgabe zu vergleichen, erzeugen Sie zunächst ein Array aus der Ausgabe von »array_to_lines()«. Anschließend iterieren Sie über das Eingabe-Array und prüfen für jedes Element, ob es im Ausgabe-Array enthalten ist.
Diese Hilfsfunktion nennen Sie »_test_array_to_lines_ausgabe_vollstaendig« und rufen sie mittels »When Call« aus dem Testcase heraus auf. Mit dem Ausdruck »The status should equal 0« teilen Sie Shell-Spec mit, dass der erwartete Rückgabewert der Funktion 0 ist. Den – zugegeben etwas unhandlichen – Testcase zeigt Listing 5.
Listing 5
Shell-Code in Specfiles
It "gibt jedes Element aus dem Eingabearray aus"
_test_array_to_lines_ausgabe_vollstaendig() {
local eingabe
local ausgabe
local eingabe_element
eingabe=(
0 1 2 3 4 5 6 7
"" " " "-" "--"
"a" "b" "c" "d"
)
ausgabe=()
readarray -t ausgabe < <(array_to_lines
"${eingabe[@]}")
for eingabe_element in "${eingabe[@]}"; do
local -i ist_in_ausgabe
local ausgabe_element
ist_in_ausgabe=0
for ausgabe_element in "${ausgabe[@]}"; do
if [[ "$eingabe_element" == "$ausgabe_element" ]]; then
ist_in_ausgabe=1
break
fi
done
if (( ist_in_ausgabe == 0 )); then
return 1
fi
done
return 0
}
When call _test_array_to_lines_ausgabe_vollstaendig
The status should equal 0
End
Mit dem Schlüsselwort »The« lassen sich auch Bedingungen für andere Aspekte einer Funktion oder eines Befehls definieren. Wollen Sie beispielsweise die Standardausgabe mit einer Zeichenkette vergleichen, benutzen Sie den Ausdruck »The stdout should equal “Zeichenkette“«.
Der nächste Testcase soll die Anzahl der Zeilen in der Standardausgabe prüfen. Das erledigen Sie mit dem Ausdruck »The lines of stdout should equal«, gefolgt von einer Ganzzahl. Der Testcase für die Länge der Ausgabe von »array_to_lines()« lässt sich in nur vier Zeilen realisieren, wie in Listing 6 zu sehen ist. Bei der Übergabe eines Arrays an eine Funktion werden die Elemente als separate Parameter übergeben. Daher können Sie die Elemente auch direkt an die Funktion überreichen und müssen nicht erst ein Array deklarieren.
Listing 6
Die Länge der Ausgabe testen
It "gibt so viele Elemente aus, wie das Eingabearray lang ist" When call array_to_lines 1 2 3 4 5 The lines of stdout should equal 5 End
Mit derselben Bedingung wie in Listing 6 könnten Sie auch den Testcase implementieren, der sicherstellt, dass die Funktion nichts ausgibt, wenn das Eingabearray leer ist. Stattdessen können Sie aber auch einfach die Bedingung für die Standardausgabe weglassen. Auf diese Weise teilen Sie Shell-Spec mit, dass Sie keine Daten auf der Standardausgabe erwarten. Da aber mindestens eine Bedingung definiert werden muss, teilen Sie Shell-Spec mit, dass die Funktion 0 zurückgeben soll (Listing 7).
Listing 7
Es wird keine Ausgabe erwartet
It "gibt nichts aus, wenn das Eingabearray leer ist" When call array_to_lines The status should equal 0 End
Mit der Bedingung »line of« können Sie auch einzelne Zeilen der Standardausgabe vergleichen. Soll beispielsweise die Zeichenkette »Hallo Welt« die erste Zeile der Standardausgabe sein, verwenden Sie die Bedingung »The line 1 of stdout should equal “Hallo Welt”«. In Listing 8 stellt sie sicher, dass »array_to_lines()« die Reihenfolge der Elemente nicht verändert.
Listing 8
Reihenfolge der Elemente testen
It "aendert nicht die Reihenfolge der Elemente" When call array_to_lines 1 2 3 The line 1 of stdout should equal "1" The line 2 of stdout should equal "2" The line 3 of stdout should equal "3" End
Somit ist nicht nur geklärt, wie die ersten zwei Testcases für »array_sort()« aussehen müssen: Sie haben auch alles an der Hand, was Sie brauchen, um zu testen, ob »array_sort()« zeichenweise sortiert. Wie bereits dargelegt, ändert zeichenweises Sortieren die Reihenfolge der Zahlen 1, 3, 20 in 1, 20, 3. Sie müssen also lediglich mit »line of« testen, ob die Ausgabe die gewünschte Reihenfolge hat. Listing 9 macht es vor.
Listing 9
Ausgabereihenfolge testen
It "sortiert nicht zeichenweise" When call array_sort 20 1 3 The line 1 of stdout should equal "1" The line 2 of stdout should equal "3" The line 3 of stdout should equal "20" End
Mit den Bedingungen, die Sie in den bisherigen Testcases kennengelernt haben, können Sie auch im Handumdrehen die Testcases für die Funktion »array_contains()« schreiben. Dazu brauchen Sie weder komplizierte Hilfsfunktionen noch Vergleiche: Es genügt, lediglich den Rückgabewert zu testen. Wie schon in Listing 6 gezeigt, übergeben Sie das Array direkt als Menge von Parametern, was erlaubt, den Aufruf in einer einzigen Zeile unterzubringen. Listing 10 fasst die fünf Testcases von »array_contains()« zusammen.
Listing 10
Die Testcases
Describe "array_contains()"
It "findet ein Element am Anfang des Arrays"
When call array_contains "1" "1" "2" "3"
The status should equal 0
End
It "findet ein Element in der Mitte des Arrays"
When call array_contains "2" "1" "2" "3"
The status should equal 0
End
It "findet ein Element am Ende des Arrays"
When call array_contains "3" "1" "2" "3"
The status should equal 0
End
It "gibt 1 zurueck, wenn ein Element nicht im Eingabearray ist"
When call array_contains "0" "1" "2" "3"
The status should equal 1
End
It "gibt 1 zurueck, wenn das Array leer ist"
When call array_contains "0"
The status should equal 1
End
End
Test-Framework trifft Modul-Framework
Wenn Sie die Testcases nun in einem Specfile zusammenfassen, können Sie sie dennoch nicht ausführen, da die zu testenden Funktionen nicht innerhalb des Specfiles definiert wurden. Sie müssen auch unser Modul-Framework samt Array-Modul im Specfile laden.
Das sich in Specfiles Shell-Ausdrücke verwenden lassen, genügt es, wenn Sie vor der ersten Testgruppe das Modul-Framework laden, indem Sie die »include«-Funktion aufrufen. Da sich Shell-Spec lautstark beschwert, falls eine der Funktionen einen Fehler ausgibt oder auch nur auf die Standardausgabe schreibt, ist dies einer der wenigen Orte, an denen Sie eine Fehlerbehandlung weglassen können. Das fertige Specfile sehen Sie in Listing 11.
Listing 11
Specfile für das Array-Modul
. bms.sh
include "array"
Describe "array_to_lines()"
It "gibt jedes Element aus dem Eingabearray aus"
_test_array_to_lines_ausgabe_vollstaendig() {
local eingabe
local ausgabe
local eingabe_element
eingabe=(
0 1 2 3 4 5 6 7
"" " " "-" "--"
"a" "b" "c" "d"
)
ausgabe=()
readarray -t ausgabe < <(array_to_lines "${eingabe[@]}")
for eingabe_element in "${eingabe[@]}"; do
local -i ist_in_ausgabe
local ausgabe_element
ist_in_ausgabe=0
for ausgabe_element in "${ausgabe[@]}"; do
if [[ "$eingabe_element" == "$ausgabe_element" ]]; then
ist_in_ausgabe=1
break
fi
done
if (( ist_in_ausgabe == 0 )); then
return 1
fi
done
return 0
}
When call _test_array_to_lines_ausgabe_vollstaendig
The status should equal 0
End
It "gibt so viele Elemente aus, wie das Eingabearray lang ist"
When call array_to_lines 1 2 3 4 5
The lines of stdout should equal 5
End
It "gibt nichts aus, wenn das Eingabearray leer ist"
When call array_to_lines
The status should equal 0
End
It "aendert nicht die Reihenfolge der Elemente"
When call array_to_lines 1 2 3
The line 1 of stdout should equal "1"
The line 2 of stdout should equal "2"
The line 3 of stdout should equal "3"
End
End
Describe "array_sort()"
It "gibt nichts aus, wenn das Eingabearray leer ist"
When call array_to_lines
The status should equal 0
End
It "gibt jedes Element aus dem Eingabearray aus"
_test_array_sort_ausgabe_vollstaendig() {
local eingabe
local ausgabe
local eingabe_element
eingabe=(
0 1 2 3 4 5 6 7
"" " " "-" "--"
"a" "b" "c" "d"
)
ausgabe=()
readarray -t ausgabe < <(array_sort "${eingabe[@]}")
for eingabe_element in "${eingabe[@]}"; do
local -i ist_in_ausgabe
local ausgabe_element
ist_in_ausgabe=0
for ausgabe_element in "${ausgabe[@]}"; do
if [[ "$eingabe_element" == "$ausgabe_element" ]]; then
ist_in_ausgabe=1
break
fi
done
if (( ist_in_ausgabe == 0 )); then
return 1
fi
done
return 0
}
When call _test_array_sort_ausgabe_vollstaendig
The status should equal 0
End
It "sortiert nicht zeichenweise"
When call array_sort 20 1 3
The line 1 of stdout should equal "1"
The line 2 of stdout should equal "3"
The line 3 of stdout should equal "20"
End
End
Describe "array_contains()"
It "findet ein Element am Anfang des Arrays"
When call array_contains "1" "1" "2" "3"
The status should equal 0
End
It "findet ein Element in der Mitte des Arrays"
When call array_contains "2" "1" "2" "3"
The status should equal 0
End
It "findet ein Element am Ende des Arrays"
When call array_contains "3" "1" "2" "3"
The status should equal 0
End
It "gibt 1 zurueck, wenn ein Element nicht im Eingabearray ist"
When call array_contains "0" "1" "2" "3"
The status should equal 1
End
It "gibt 1 zurueck, wenn das Array leer ist"
When call array_contains "0"
The status should equal 1
End
End
Nun geht es an den Nageltest: Hält unser Modul den Testcases stand? Zum Testen speichern Sie das Specfile unter dem Namen »listing11_spec.sh« und rufen es mit der Befehlszeile »shellspec –shell bash –format documentation listing11_spec.sh« auf. Mit der Option »–format documentation« erzeugt Shell-Spec eine anschauliche Übersicht aller ausgeführten Testcases (Abbildung 1).
Sie erkennen so schnell, dass die Tests erfolgreich absolviert wurden und das Modul alle Prüfungen bestanden hat. Da wir von nun an für alle unsere Module Tests schreiben werden, erstellen Sie ein neues Verzeichnis namens »/usr/local/share/bms/test/«, in dem Sie das Specfile unter dem Namen »array_spec.sh« ablegen.
Reguläre Bash-Features
Anhand der Tests für das Array-Modul haben Sie gelernt, wie man einfache Specfiles für Shell-Spec schreibt. Warum wir ausgerechnet Shell-Spec verwenden und nicht etwa das bekanntere BATS, wird deutlich, wenn es daran geht, Tests mit vielen verschiedenen Eingaben auszuführen. Um herauszufinden, wie man mit Shell-Spec parametrisierte Tests schreibt, wollen wir zunächst ein neues Modul entwickeln: Das »is«-Modul soll Funktionen zum Validieren von Strings enthalten.
Mit der Funktion »is_digits()« soll der Nutzer testen können, ob ein String ausschließlich aus Ziffern besteht. Die Funktionen »is_upper()« und »is_lower()« ermitteln, ob ein String ausschließlich aus Klein- beziehungsweise Großbuchstaben besteht. Zuletzt vereinigt »is_alnum()« die drei Funktionen und prüft, ob ein String ausschließlich Buchstaben und Ziffern enthält.
Öffnen Sie also »/usr/local/share/bms/include/is.sh« in einem Editor, und fangen Sie mit »is_digits()« an. Eine Variante wäre, jedes Zeichen des Strings separat zu betrachten und ASCII-Werte zu vergleichen. Für strikt POSIX-kompatible Shell-Skripte wäre das sicher ein akzeptabler Ansatz. Da unser Modul-Framework ohnehin nur mit der Bash richtig funktioniert, lässt sich aber auch eine Komfortfunktion der Bash verwenden: reguläre Ausdrücke (Regexe).
Ob es sich bei einem Zeichen um eine Ziffer handelt, ermittelt der reguläre Ausdruck »[0-9]«. Ergänzen Sie ihn um das Suffix »+«, akzeptiert er ein oder mehrere Ziffern. Um sicherzustellen, dass vor oder nach den gefundenen Ziffern keine anderen Zeichen kommen, müssen Sie den Ausdruck noch am Anfang beziehungsweise Ende des Strings verankern. Das klappt mit »^« und »$«. Den resultierenden Ausdruck vergleichen Sie nun mithilfe des Operators »=~« in einem String-Kontext mit der Eingabe (Listing 12).
Listing 12
Vergleich mit Regex
is_digits() {
local str="$1"
if [[ "$str" =~ ^[0-9]+$ ]]; then
return 0
fi
return 1
}
Die Funktionen »is_upper()« und »is_lower()« unterscheiden sich nur im Detail: Sie verwenden lediglich in einem Fall »[A-Z]« als inneren Ausdruck und im anderen »[a-z]« (Listing 13). Um »is_alnum()« zu entwickeln, könnten Sie jetzt auf diese drei Funktionen zurückgreifen. Eine performantere Lösung bietet aber das Zusammenziehen der drei Regexe (Listing 14).
Listing 13
is_upper() und is_lower()
is_upper() {
local str="$1"
if [[ "$str" =~ ^[A-Z]+$ ]]; then
return 0
fi
return 1
}
is_lower() {
local str="$1"
if [[ "$str" =~ ^[a-z]+$ ]]; then
return 0
fi
return 1
}
Listing 14
is_alnum()
is_alnum() {
local str="$1"
if [[ "$str" =~ ^[a-zA-Z0-9]+ ]]; then
return 0
fi
return 1
}
Zum Schluss erhält das neue Modul noch einen Konstruktor, der auch dieses Mal nur 0 zurückgeben muss. Wollen Sie das Modul nun auf dieselbe Weise testen wie das Array-Modul, müssen Sie entweder viel tippen oder eine ausgeklügelte Hilfsfunktion schreiben. Es gibt aber noch eine einfachere Möglichkeit.
Am laufenden Band
Anders als bei BATS können Sie in Shell-Spec dynamisch Parameter für Tests erzeugen. Dazu setzen Sie in eine Testgruppe das Schlüsselwort »Parameters:dynamic«. Zwischen diesem Schlüsselwort und dem nächsten »End« können Sie mit der Spezialfunktion »%data« Werte definieren, auf die Sie in den Testcases über Positionsparameter zugreifen. Stellen Sie sich das so vor, dass jeder Aufruf von »%data« die Testcases in der Testgruppe aufruft, wobei die Parameter direkt an die Testcases weitergereicht werden.
Innerhalb des »Parameters«-Blocks können Sie auch Schleifen, Subshells und andere Konstrukte verwenden, was es erlaubt, die Tests für die »is_digits()« Funktion wie in Listing 15 zu definieren: Im »Parameters«-Block iterieren Sie über alle sichtbaren ASCII-Werte (von 32 bis ausschließlich 127) und ermitteln mit »printf« das dazugehörige Zeichen. Die Werte von 48 bis einschließlich 57 entsprechen den Ziffern 0 bis 9, für die Sie einen akzeptierenden Test anlegen.
Als Argumente übergeben Sie an »%data« den Namen des Tests, die zu testende Eingabe sowie das erwartete Ergebnis. Für alle anderen ASCII-Zeichen erzeugen Sie Tests, bei denen die Eingabe nicht akzeptiert werden soll. Entsprechend ändern Sie den Titel und den erwarteten Rückgabewert. Im Testcase, den Sie wie gewohnt mit »It« definieren, greifen Sie mit Positionsparametern auf die Argumente von »%data« zu. Führen Sie das Specfile nun aus, greift der Testcase wie in Abbildung 2 angedeutet für jedes sichtbare ASCII-Zeichen.
Listing 15
Parametrisierter Test
. bms.sh
include "is"
Describe "is_digits()"
Parameters:dynamic
for (( i = 32; i < 127; i++ )); do
c=$(printf "\\x$(printf '%02x' "$i")")
if (( i >= 48 )) && (( i <= 57 )); then
%data "akzeptiert '$c'" "$c" 0
else
%data "akzeptiert '$c' nicht" "$c" 1
fi
done
End
It "$1"
When call is_digits "$2"
The status should equal "$3"
End
End
Auf dieselbe Weise schreiben Sie auch die Tests für »is_upper()« und »is_lower()« im Handumdrehen. Dabei müssen Sie lediglich die Grenzen für die akzeptierenden Tests anpassen: 65 bis 90 für Großbuchstaben und 97 bis 122 für Kleinbuchstaben. Der Test für »is_alnum()« muss alle drei Intervalle akzeptieren, wobei das logische Oder hilft. Listing 16 zeigt das fertige Specfile.
Listing 16
Specfile des Is-Moduls
. bms.sh
include "is"
Describe "is_digits()"
Parameters:dynamic
for (( i = 32; i < 127; i++ )); do
c=$(printf "\\x$(printf '%02x' "$i")")
if (( i >= 48 )) && (( i <= 57 )); then
%data "akzeptiert '$c'" "$c" 0
else
%data "akzeptiert '$c' nicht" "$c" 1
fi
done
End
It "$1"
When call is_digits "$2"
The status should equal "$3"
End
End
Describe "is_upper()"
Parameters:dynamic
for (( i = 32; i < 127; i++ )); do
c=$(printf "\\x$(printf '%02x' "$i")")
if (( i >= 65 )) && (( i <= 90 )); then
%data "akzeptiert '$c'" "$c" 0
else
%data "akzeptiert '$c' nicht" "$c" 1
fi
done
End
It "$1"
When call is_upper "$2"
The status should equal "$3"
End
End
Describe "is_lower()"
Parameters:dynamic
for (( i = 32; i < 127; i++ )); do
c=$(printf "\\x$(printf '%02x' "$i")")
if (( i >= 97 )) && (( i <= 122 )); then
%data "akzeptiert '$c'" "$c" 0
else
%data "akzeptiert '$c' nicht" "$c" 1
fi
done
End
It "$1"
When call is_lower "$2"
The status should equal "$3"
End
End
Describe "is_alnum()"
Parameters:dynamic
for (( i = 32; i < 127; i++ )); do
c=$(printf "\\x$(printf '%02x' "$i")")
if (( i >= 48 && i <= 57 )) ||
(( i >= 65 && i <= 90 )) ||
(( i >= 97 && i <= 122 )); then
%data "akzeptiert '$c'" "$c" 0
else
%data "akzeptiert '$c' nicht" "$c" 1
fi
done
End
It "$1"
When call is_alnum "$2"
The status should equal "$3"
End
End
Führen Sie das Specfile nun mit Shell-Spec wie in Abbildung 3 gezeigt aus, erfolgen in einem Rutsch 380 Tests auf einmal – wofür Sie nur drei Testcases definieren mussten.
Fazit
Aufgrund der fehlenden Modularisierung sind traditionelle Bash-Skripte nicht nur mühsam zu pflegen, auch Tests erfordern erheblichen Aufwand. Unser Modulsystem reduziert zum einen den Entwicklungsaufwand für Skripte und verbessert zum anderen deren Verlässlichkeit, da wir nicht nur ganze Skripte testen können, sondern flexibel sogar beliebige einzelne Funktionen. Das hier vorgestellte Test-Framework Shell-Spec bietet mit seinen parametrisierten Tests eine einfache Möglichkeit, eine große Anzahl von Tests zu schreiben, und ist somit eine ideale Ergänzung für unser Modulsystem. (jcb/jlu)
Infos
- Shell-Spec: https://shellspec.info









