Unit-Test für Shell-Skripte


74

Nahezu jedes Produkt, an dem ich im Laufe der Jahre gearbeitet habe, enthielt einige Shell-Skripte (oder Batch-Dateien, PowerShell usw. unter Windows). Obwohl wir den Großteil des Codes in Java oder C ++ geschrieben haben, schien es immer einige Integrations- oder Installationsaufgaben zu geben, die besser mit einem Shell-Skript erledigt werden konnten.

Die Shell-Skripte werden somit Teil des ausgelieferten Codes und müssen daher genau wie der kompilierte Code getestet werden. Hat jemand Erfahrung mit einigen der verfügbaren Shell-Skript-Unit-Test-Frameworks, wie z. B. shunit2 ? Ich interessiere mich momentan hauptsächlich für Linux-Shell-Skripte. Ich möchte wissen, wie gut das Testkabel die Funktionalität und Benutzerfreundlichkeit anderer xUnit-Frameworks dupliziert und wie einfach es ist, es in Continuous-Build-Systeme wie CruiseControl oder Hudson zu integrieren.


Roundup formalisiert einige Aufgaben / Tags für Sie. Sobald Sie den Lernbuckel überwunden haben, ist es sehr nützlich. Persönlich mag ich den Ansatz von haridsv besser, weil ich kein weiteres Paket installieren muss. Ich habe diesen Ansatz auf Shell-Skript- und Python-Tests angewendet.
todo

1
Sie können auch bashtest util überprüfen: github.com/pahaz/bashtest (es ist eine einfache Möglichkeit, Bash-Tests zu schreiben)
pahaz

1
Schauen Sie sich diese Übersicht über fast alle möglichen Tools an: medium.com/wemake-services/…
sobolevn

@sobolevn sehr schöner Artikel, danke!
gareth_bowles

Antworten:


52

UPDATE 2019-03-01: Ich bevorzuge jetzt Fledermäuse . Ich habe es für ein paar Jahre bei kleinen Projekten verwendet. Ich mag die saubere, prägnante Syntax. Ich habe es nicht in CI / CD-Frameworks integriert, aber sein Exit-Status spiegelt den Gesamterfolg / Misserfolg der Suite wider, der besser ist als shunit2, wie unten beschrieben.


VORHERIGE ANTWORT:

Ich verwende shunit2 für Shell-Skripte, die sich auf eine Java / Ruby-Webanwendung in einer Linux-Umgebung beziehen. Es war einfach zu bedienen und keine große Abweichung von anderen xUnit-Frameworks.

Ich habe nicht versucht, mich in CruiseControl oder Hudson / Jenkins zu integrieren, aber bei der Implementierung einer kontinuierlichen Integration auf andere Weise bin ich auf folgende Probleme gestoßen:

  • Beendigungsstatus: Wenn eine Testsuite fehlschlägt, verwendet shunit2 keinen Beendigungsstatus ungleich Null, um den Fehler zu kommunizieren. Sie müssen also entweder die Ausgabe von shunit2 analysieren, um das Bestehen / Nichtbestehen einer Suite zu bestimmen, oder shunit2 so ändern, dass es sich so verhält, wie es einige Frameworks für die kontinuierliche Integration erwarten, und Pass / Fail über den Exit-Status kommunizieren.
  • XML-Protokolle: shunit2 erstellt kein XML-Ergebnisprotokoll im JUnit-Stil.

Pete, ich bin kurz davor, eine Unit-Test-Bibliothek zu veröffentlichen, die für Bash und Ksh funktioniert und sehr einfach zu bedienen ist. Können Sie mir irgendwo einen Link zum XML-Protokoll im Junit-Stil senden, damit ich sehen kann, was es enthält? Möchten Sie auch den Beendigungsstatus für einen einzelnen fehlgeschlagenen Test oder am Ende nach Abschluss, wenn> 0 Test fehlgeschlagen ist, beenden Sie 1? Ich mache es jetzt so, dass es eine Funktion gibt, die Sie aufrufen können, um die Anzahl der Tests zu ermitteln, die bestanden / nicht bestanden wurden.
Ethan Post

@EthanPost Ich verwende / benötige keine XML-Protokolle mehr. Sie können Hilfe finden, indem Sie nach Dingen wie "xunit xml format" googeln. Dinge wie diese: xunit.github.io/docs/format-xml-v2.html
Pete TerMaat

2
shunit wird jetzt auch mit dem richtigen Exit-Code beendet: github.com/kward/shunit2/blob/…
Flamefire

31

Ich frage mich, warum niemand BATS erwähnt hat . Es ist aktuell und TAP- konform.

Beschreiben:

#!/usr/bin/env bats

@test "addition using bc" {
  result="$(echo 2+2 | bc)"
  [ "$result" -eq 4 ]
}

Lauf:

$ bats addition.bats
 ✓ addition using bc

1 tests, 0 failures

1
+1 für BATS! Gemessen an den Github-Metriken (12 Mitwirkende, fast 2000 Sterne) ist dies das Tool, das die meisten Leute als ihren Bash xUnit-Testläufer auswählen.
Noah Sussman

1
+1: Ich habe vor einiger Zeit BATS benutzt und war nicht ganz überwältigt. Fand dieses ausgezeichnete (unabhängige?) Getting Started-Dokument heute, folgte es durch und bin jetzt vollständig auf BATS verkauft.
ssc

Als ich diese Antwort gelesen habe, war mein erster Impuls "Nein, der Betreuer reagiert nicht auf Probleme und es gibt einige schwerwiegende Fehler, aber dann habe ich festgestellt, dass es gegabelt wurde, seit ich es das letzte Mal verwendet habe"
Andy

21

Roundup von @ blake-mizerany klingt großartig, und ich sollte es in Zukunft nutzen, aber hier ist mein "Poor-Man" -Ansatz für die Erstellung von Unit-Tests:

  • Trennen Sie alles, was als Funktion testbar ist.
  • Verschieben Sie Funktionen beispielsweise in eine externe Datei functions.shund sourcein das Skript. Sie können source `dirname $0`/functions.shfür diesen Zweck verwenden.
  • functions.shBetten Sie Ihre Testfälle am Ende von in die folgende Bedingung ein:

    if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    fi
    
  • Ihre Tests sind wörtliche Aufrufe der Funktionen, gefolgt von einfachen Überprüfungen auf Exit-Codes und Variablenwerte. Ich möchte eine einfache Dienstprogrammfunktion wie die folgende hinzufügen, um das Schreiben zu vereinfachen:

    function assertEquals()
    {
        msg=$1; shift
        expected=$1; shift
        actual=$1; shift
        if [ "$expected" != "$actual" ]; then
            echo "$msg EXPECTED=$expected ACTUAL=$actual"
            exit 2
        fi
    }
    
  • Führen Sie schließlich functions.shdirekt aus, um die Tests auszuführen.

Hier ist ein Beispiel, um den Ansatz zu zeigen:

    #!/bin/bash
    function adder()
    {
        return $(($1+$2))
    }

    (
        [[ "${BASH_SOURCE[0]}" == "${0}" ]] || exit 0
        function assertEquals()
        {
            msg=$1; shift
            expected=$1; shift
            actual=$1; shift
            /bin/echo -n "$msg: "
            if [ "$expected" != "$actual" ]; then
                echo "FAILED: EXPECTED=$expected ACTUAL=$actual"
            else
                echo PASSED
            fi
        }

        adder 2 3
        assertEquals "adding two numbers" 5 $?
    )

1
Schön, danke - ich mag den strukturierten Programmieransatz für Shell-Skripte sehr.
gareth_bowles

Sehr gute Beobachtung dort: "Trenne alles, was als Funktion testbar ist". Es ist wichtig, auch wenn Sie (noch) keinen Test dafür schreiben. Dies ist ein wichtiger Faktor für die Verbesserung der Lesbarkeit von Code. Vor ein paar Jahren begann ich mein (ksh) Skript nach diesem Prinzip zu schreiben. Schreiben Sie alles als Funktion.
Henk Langeveld

@HenkLangeveld Wird nicht auch die exit 0"Quelle" (diejenige, die dies bezieht) beendet? Ich denke du suchst returnhier.
Haridsv

@haridsv Gute Frage. Wenn Sie die Datei als Quelle verwenden, möchten Sie die Tests nicht ausführen. Durch das Sammeln des gesamten testbezogenen Codes in einer Subshell wird die Laufzeitumgebung des "Verbrauchers" nicht beeinträchtigt. Das exit 0beendet nur die Subshell.
Henk Langeveld

1
Das ist brilliant. Danke für die ausführliche Erklärung.
Bilderstürmer

9

Neben Roundup und shunit2 meine Übersicht über Shell - Einheit Test - Tools ebenfalls enthalten assert.sh und shelltestrunner .

Ich stimme größtenteils der Kritik des Roundup-Autors an shunit2 zu (einige davon subjektiv), daher habe ich shunit2 ausgeschlossen, nachdem ich mir die Dokumentation und Beispiele angesehen habe. Obwohl es mir bekannt vorkam, einige Erfahrungen mit jUnit zu haben.

Meiner Meinung nach ist shelltestrunner das originellste der Tools, die ich mir angesehen habe, da es eine einfache deklarative Syntax für die Definition von Testfällen verwendet. Wie üblich bietet jede Abstraktionsebene eine gewisse Bequemlichkeit auf Kosten einer gewissen Flexibilität. Obwohl die Einfachheit attraktiv ist, fand ich das Tool für den Fall, den ich hatte, zu einschränkend, hauptsächlich weil es keine Möglichkeit gab, Setup- / TearDown-Aktionen zu definieren (z. B. Eingabedateien vor einem Test bearbeiten, Statusdateien nach einem Test entfernen , etc.).

Ich war zunächst ein wenig verwirrt, dass assert.sh nur das Aktivieren des Ausgabe- oder Exit-Status erlaubt, während ich beides brauchte. Lange genug, um ein paar Testfälle mit Roundup zu schreiben. Ich fand den set -eModus der Zusammenfassung jedoch bald unpraktisch, da in einigen Fällen ein Exit-Status ungleich Null erwartet wird, um das Ergebnis zusätzlich zu stdout zu kommunizieren, wodurch der Testfall in diesem Modus fehlschlägt. Eine der Proben zeigt die Lösung:

status=$(set +e ; rup roundup-5 >/dev/null ; echo $?)

Aber was ist, wenn ich sowohl den Exit-Status ungleich Null als auch den Ausgang benötige? Ich könnte natürlich set +evor dem Aufruf und set -enach oder set +efür den gesamten Testfall. Aber das widerspricht dem Prinzip der Zusammenfassung "Alles ist eine Behauptung" . Es fühlte sich also so an, als würde ich anfangen, gegen das Werkzeug zu arbeiten.

Bis dahin habe ich erkannt, dass der "Nachteil" von assert.sh, nur den Exit-Status oder die Ausgabe zu bestätigen, eigentlich kein Problem ist, da ich nur testmit einem zusammengesetzten Ausdruck wie diesem übergeben kann

output=$($tested_script_with_args)
status=$?
expected_output="the expectation"
assert_raises "test \"$output\" = \"$expected_output\" -a $status -eq 2"

Da meine Anforderungen wirklich grundlegend waren (eine Reihe von Tests durchführen, anzeigen, dass alles in Ordnung war oder was fehlgeschlagen ist), mochte ich die Einfachheit von assert.sh, also habe ich mich für diese entschieden.


6

Ich habe kürzlich ein neues Test-Framework namens Shellspec veröffentlicht.

Shellspec ist ein Testframework im BDD-Stil. Es funktioniert mit POSIX-kompatiblen Shell-Skripten wie Bash, Dash, Ksh, Busybox usw.

Natürlich spiegelt der Exit-Status das Ergebnis der Ausführung der Spezifikationen wider und verfügt über einen TAP-kompatiblen Formatierer.

Die Spezifikationsdatei kommt der natürlichen Sprache nahe und ist leicht zu lesen. Außerdem ist sie mit Shell-Skripten kompatibel.

#shellcheck shell=sh

Describe 'sample'
  Describe 'calc()'
    calc() { echo "$(($*))"; }

    It 'calculates the formula'
      When call calc 1 + 2
      The output should equal 3
    End
  End
End

Unterstützter JUnit-Formatierer in Version 0.16.0. und unterstützte auch Abdeckung, parallele Ausführung und mehr.
Koichi Nakashima

3

Sie sollten die assert.sh lib ausprobieren , sehr praktisch, einfach zu bedienen

local expected actual
expected="Hello"
actual="World!"
assert_eq "$expected" "$actual" "not equivalent!"
# => x Hello == World :: not equivalent! 
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.