Bash Automated Testing System unter Unix

Unit-Tests für Bash-Skripte mit BATS

| Autor / Redakteur: Mirco Lang / Stephan Augsten

Ein simpler BATS-Testlauf mit einem nicht bestandenen Test.
Ein simpler BATS-Testlauf mit einem nicht bestandenen Test. (Bild: Lang / BATS)

Beim Thema Software Testing stehen normalerweise die großen, „echten“ Programmiersprachen im Vordergrund – Skripte bleiben meist außen vor. Mit dem Skript-Spezialisten BATS lassen sich ganz einfach Unit-Tests für jegliche Art von Unix-Programm erstellen.

Ein Vorteil von Bash Automated Testing System (BATS) ist, dass das Testing-Framework selbst im Falle kleiner Skripte keinen wirklichen Overhead verursacht. Obendrein ist BATS der perfekte Einstieg in die Welt des automatisierten Testens.

In wenigen Minuten versteht auch der geneigte Einsteiger ohne Vorkenntnisse, was denn letztlich ganz praktisch hinter Testing-Fachbegriffen wie Automated Testing, Unit-Tests oder Framework steht. BATS basiert auf dem ursprünglich für Perl gedachten Test Anything Protocol (TAP), das eine einfache, textbasierte Schnittstelle zwischen Unit-Test und Testing Harness/Framework realisiert – was auch schon wieder komplizierter klingt, als es ist.

Die Arbeitsweise von BATS ist im Grunde geradezu trivial: Über die Test-Definitionen werden Befehle, Programme oder kleinere Logiken wie Schleifen und Vergleiche mit den üblichen Bash-Utensilien erstellt. BATS-Test-Definitionen sind nämlich auch nur Bash-Skripte.

Zu den ausgeführten Programmen werden Bedingungen definiert, die entweder erfüllt werden oder eben nicht, was dann wiederum im Testprotokoll als Fehler auftaucht. Man könnte BATS also zum Beispiel testen lassen, ob ein gegebenes Skript mit gegebenen Argumenten ohne Fehlermeldung durchläuft und einen bestimmten Output liefert – wie einfach das ist, zeigen wir in den folgenden Schritten.

Ein erster Test mit BATS

Das originale BATS findet sich auf GitHub – ebenso wie BATS-Core, ein etwas aktuellerer, erweiterter Fork. Für den Einstieg soll es aber beim Original bleiben, zumal das unter Ubuntu, Debian &und Co. auch direkt aus den Paketquellen installiert werden kann, das übliche

sudo apt-get install bats

genügt bereits. Am besten legt man sich eine kleine Testumgebung an. Für die folgenden Beispiele benötigen wir einige Ordner und Dateien:

Ordner „~/bats“ mit einigen Textdateien (*.txt) mit Testinhalten („hallo welt“, „foo“, „bar“ und so weiter) sowie BATS-Dateien (test.bats). Hinzu kommt der Ordner „~/bats/testordner“ und darin nur die Datei „testdatei“. Später kommt noch ein Mini-Skript namens „skrippt2“ in den bats-Ordner.

Für einen ersten, minimalistischen Test soll geprüft werden, ob das Kommando „ls“ funktioniert. Erstellen Sie eine BATS-Datei „test.bats“ mit folgendem Inhalt:

@test "ls-Test" {
   ls
}

Tests werden grundsätzlich mit „@test“ eingeleitet, gefolgt von einer Beschreibung, die später auch in der Ausgabe auftaucht. Der eigentliche Test läuft dann in den geschweiften Klammern. Wichtig: Die schließende Klammer muss in einer neuen Zeile stehen. Der Test ist hier der Aufruf von „ls“ – sofern das Kommando ohne Fehlermeldung durchläuft, gibt es den Statuscode „0“ zurück und der Test gilt als bestanden.

Und mehr macht BATS im Grunde nicht: Was auch immer hier an Tests formuliert wird, BATS wertet schlicht die zurückgegebenen Statuscodes aus, kann also Ja-Nein-Fragen beantworten. Wenn Sie die Datei nun mit „bats test.bats“ ausführen, bekommen Sie eine hübsch formatierte Ausgabe, die kundtut, dass der Test erfolgreich durchgelaufen ist.

Testfälle formulieren

BATS verfügt über einige wenige eingebaute Variablen. Der Statuscode landet beispielsweise in „$status“, was bei einem Aufruf mit dem Kommando „run“ sichtbar wird:

@test "ls-Test 2" {
   run ls ~/testordner
   [ "$status" = 0 ]
   [ "$output" = testdatei ]
}

Hier wird nun geprüft, ob der ls-Befehl fehlerfei, also mit einem zurückgegebenen Statuscode „0“, durchläuft und zusätzlich, ob die Ausgabe, gespeichert in „$output“, dem String „testdatei“ entspricht. Sofern im Ordner „testordner“ nur die Datei „testdatei“ liegt, ist das der Fall.

Standardmäßig wird in BATS über den run-Befehl gestartet, um direkt auf die Variablen zugreifen zu können. Alles, was sich in den eckigen Klammern befindet, ist eine Bedingung, im Grunde also der eigentliche Test – entsprechend kann jede dieser Zeilen separat einen Fehler in der Ausgabe produzieren.

Solange nur diese eine Datei vorhanden ist, ist der Test trivial. Was aber, wenn geprüft werden soll, ob ein Begriff irgendwo in der Ausgabe vorkommt? Dazu ein kleines Beispiel, das gleich noch weitere Aspekte verdeutlicht: Geprüft werden soll ein Skript „skrippt2“, das schlichtweg alle ihm übergebenen Dateien via „cat“ konkateniert, also aneinanderreiht. Die Bedingung: Es soll das Wort „welt“ irgendwo vorkommen.

Zunächst das Skript:

for FILE1 in "$@"
do
cat $FILE1
done

„$@“ sorgt dabei dafür, dass alle via „skrippt2 test1.txt test2.txt“ etc. übergebenen Dateien verarbeitet werden.

Der eigentliche Test:

@test "Test 3: Kommt in der Ausgabe das Wort \"welt\" vor?" {
   /home/mirco/bats/testing/skrippt2 *.txt | grep 'welt'
}

Wie zu sehen ist, wurde hier wieder ohne „run“ gearbeitet– mit gepipeten Befehlen funktioniert es schlicht nicht. Interessant ist aber eher, dass Pipes funktionieren, denn so lassen sich im Grunde alle Anforderungen als Ja-Nein-Fragen umsetzen. Neben simplen Pipes könnten an dieser Stelle auch Schleifen und umfangreichere Logiken stehen.

Nun soll der Test aber etwas präziser sein und die Frage beantworten, ob ein bestimmter Text an einer bestimmten Stelle der Ausgabe steht. So könnte man beispielsweise prüfen, ob ein Skript bei Aufruf mit einer nicht existierenden Datei in der ersten Zeile die zu erwartende „Datei nicht gefunden“-Meldung ausgibt. Hier soll erstmal nur geprüft werden, ob das Wort „foo“ in Zeile 3 steht:

@test "Steht in der Skriptausgabe \"foo\" in Zeile 3?" {
   run /home/mirco/bats/testing/skrippt2 *.txt
   [ "${lines[2]}" = "foo" ]

}

Hier wird wieder ohne Pipe, dafür mit dem run-Befehl gearbeitet. Und dieser bietet abermals eine neue Variable: „lines“ ist ein Array, über das jede einzelne Zeile der Ausgabe separat angesprochen werden kann. Und da die Zählung natürlich bei Null beginnt, entspricht Zeile 3 hier eben „${lines[2]}“. Und sofern beim Konkatenieren Ihrer Test-Textdateien in Zeile 3 (nur) das Wort „foo“ steht, ist der Test wieder bestanden.

Abseits der Tests

Wie es sich für ein gutes Rahmenwerk gehört, beherrscht auch BATS die Fähigkeit, Befehle vor und nach Testablauf auszuführen, beispielsweise um die Testumgebung aufzubauen. Die Befehle dazu lauten „setup“ und „teardown“ und können irgendwo im Skript stehen:

setup() {
   touch 1 2
}

teardown() {
   rm 1 2
}

Zu Demonstrationszwecken werden hier also einfach die Dateien „1“ und „2“ angelegt und nach dem Test wieder gelöscht – stattdessen lassen sich zum Beispiel Verzeichnisse anlegen, Log-Dateien einlesen oder ganze Testumgebungen aufbauen.

Wenn Sie einzelne Tests nicht ausführen wollen, können Sie diese einfach über den Befehl „skip“ auslassen:

@test "ls-Test" {
   skip
   ls
}

Und noch eine letzte Option abseits der Tests wartet auf Sie: BATS-Skripte können auch Befehle außerhalb von „@test“, „setup()“ und „teardown()“ enthalten. Derartiger Code muss dann allerdings nach „stdrr“ (>&2) umgeleitet werden.

Umfangreiche BATS-Tests

Natürlich ist es möglich, all diese Elemente miteinander zu kombinieren und umfangreiche BATS-Skripte zu erstellen. Auch lassen sich mehrere BATS-Dateien gleichzeitig aufrufen, indem der bats-Befehl einfach gegen einen Ordner mit BATS-Dateien ausgeführt wird. Und eine letzte Komponente sollten Sie noch kennenlernen: Über „load“ können Sie BATS-Skripte beziehungsweise -Code-Snippets einbetten. Ein simples Test-Skript könnte also in etwas so aussehen:

#!/usr/bin/env bats

setup() {
   vorbereiten.sh
}

teardown() {
   aufraeumen.sh
}

load test-basics.bats

@test "Test 1: Läuft das Skript durch?" {
   run /home/mirco/bats/testing/skrippt2
   [ „$status“ = 0 ]
}

@test "Test 2: Kommt in der Ausgabe das Wort \"welt\" vor UND steht \"foo\" in Zeile 3?" {
   /home/mirco/bats/testing/skrippt2 *.txt | grep 'welt'
   run /home/mirco/bats/testing/skrippt2 *.txt
   [ "${lines[2]}" = "foo" ]
}

@test "Test 3: ls-Test" {
   skip
   ls
}

BATS bietet noch ein paar weitere globale Variablen wie Testnummern und -namen, die Sie für Skripte einsetzen können. Wenn Sie darüber hinausgehende Funktionen benötigen, schauen Sie ruhig einmal bei bats-core, bats-support und bats-assert vorbei.

Kommentare werden geladen....

Kommentar zu diesem Artikel

Der Kommentar wird durch einen Redakteur geprüft und in Kürze freigeschaltet.

Anonym mitdiskutieren oder einloggen Anmelden

Avatar
Zur Wahrung unserer Interessen speichern wir zusätzlich zu den o.g. Informationen die IP-Adresse. Dies dient ausschließlich dem Zweck, dass Sie als Urheber des Kommentars identifiziert werden können. Rechtliche Grundlage ist die Wahrung berechtigter Interessen gem. Art 6 Abs 1 lit. f) DSGVO.
  1. Avatar
    Avatar
    Bearbeitet von am
    Bearbeitet von am
    1. Avatar
      Avatar
      Bearbeitet von am
      Bearbeitet von am

Kommentare werden geladen....

Kommentar melden

Melden Sie diesen Kommentar, wenn dieser nicht den Richtlinien entspricht.

Kommentar Freigeben

Der untenstehende Text wird an den Kommentator gesendet, falls dieser eine Email-hinterlegt hat.

Freigabe entfernen

Der untenstehende Text wird an den Kommentator gesendet, falls dieser eine Email-hinterlegt hat.

copyright

Dieser Beitrag ist urheberrechtlich geschützt. Sie wollen ihn für Ihre Zwecke verwenden? Infos finden Sie unter www.mycontentfactory.de (ID: 45914291 / Testing)