Testen Sie Shell-Skripte

Das Schreiben von Shell-Skripten gleicht dem Programmieren. Einige Skripte erfordern wenig Zeitaufwand. Andere komplexe Skripte erfordern jedoch Nachdenken, Planung und ein größeres Engagement. Aus dieser Perspektive ist es sinnvoll, einen testgetriebenen Ansatz zu verwenden und unsere Shell-Skripts als Unit-Test zu verwenden.

Um dieses Tutorial optimal nutzen zu können, müssen Sie mit der Befehlszeilenschnittstelle (CLI) vertraut sein. Wenn Sie eine Auffrischung benötigen, sollten Sie sich das Tutorial „Die Kommandozeile ist Ihr bester Freund“ ansehen. Sie benötigen auch ein grundlegendes Verständnis für das Bash-ähnliche Shell-Scripting. Schließlich möchten Sie sich vielleicht mit den Test-Driven-Development-Konzepten (TDD-Konzepten) und Unit-Tests allgemein vertraut machen. Sehen Sie sich diese Test-Driven PHP-Tutorials an, um die Grundidee zu verstehen.


Bereiten Sie die Programmierumgebung vor

Zunächst benötigen Sie einen Texteditor, um Ihre Shell-Skripts und Komponententests zu schreiben. Verwenden Sie Ihren Favoriten!

Wir werden das ShUnit2-Shell-Unit-Test-Framework verwenden, um unsere Unit-Tests durchzuführen. Es wurde für Bash-artige Muscheln entwickelt und arbeitet mit diesen. shUnit2 ist ein Open Source-Framework, das unter der GPL-Lizenz veröffentlicht wurde. Eine Kopie des Frameworks ist auch im Beispielquellcode dieses Tutorials enthalten.

Die Installation von shUnit2 ist sehr einfach. Laden Sie das Archiv einfach herunter und extrahieren Sie es an einem beliebigen Ort auf Ihrer Festplatte. Es ist in Bash geschrieben und daher besteht das Framework nur aus Skriptdateien. Wenn Sie shUnit2 häufig verwenden möchten, empfehle ich dringend, dass Sie es an einem Ort in PATH ablegen.


Wir schreiben unseren ersten Test

Extrahieren Sie für dieses Lernprogramm shUnit in ein Verzeichnis mit demselben Namen in Ihrem Quellen Ordner (siehe den diesem Tutorial beigefügten Code). Ein ... kreieren Tests Ordner darin Quellen und fügte einen neuen Dateiaufruf hinzu firstTest.sh.

 #! / usr / bin / env sh ### firstTest.sh ### function testWeCanWriteTests () assertEquals "es funktioniert" "es funktioniert" ## Aufruf und Ausführen aller Tests. "… /Shunit2-2.1.6/src/shunit2"

Dann machen Sie Ihre Testdatei ausführbar.

$ cd __ihr Code-Ordner __ / Testet $ chmod + x firstTest.sh

Jetzt können Sie es einfach ausführen und die Ausgabe beobachten:

 $ ./firstTest.sh testWeCanWriteTests Ran 1 Test. OK

Es heißt, wir haben einen erfolgreichen Test durchgeführt. Lassen Sie uns jetzt den Test scheitern lassen; ändere das assertEquals Anweisung, so dass die beiden Zeichenfolgen nicht identisch sind und führen Sie den Test erneut aus:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: erwartet: aber war: Ran 1 Test. FAILED (Ausfälle = 1)

Ein Tennisspiel

Sie schreiben Abnahmetests zu Beginn eines Projekts / Features / einer Story, wenn Sie eine bestimmte Anforderung eindeutig definieren können.

Jetzt, da wir eine funktionierende Testumgebung haben, schreiben wir ein Skript, das eine Datei liest, auf der Grundlage des Dateiinhalts Entscheidungen trifft und Informationen auf dem Bildschirm ausgibt.

Das Hauptziel des Skripts besteht darin, die Punktzahl eines Tennisspiels zwischen zwei Spielern anzuzeigen. Wir werden uns nur auf die Punktzahl eines einzelnen Spiels konzentrieren. alles andere liegt bei dir. Die Bewertungsregeln sind:

  • Zu Beginn hat jeder Spieler eine Punktzahl von null, die "Liebe" genannt wird.
  • Erste, zweite und dritte gewonnene Kugeln werden als "fünfzehn", "dreißig" und "vierzig" markiert..
  • Wenn bei "vierzig" die Punktzahl gleich ist, spricht man von "Zwei"..
  • Danach wird die Punktzahl als "Vorteil" für den Spieler gehalten, der einen Punkt mehr als der andere Spieler erzielt.
  • Ein Spieler ist der Gewinner, wenn er einen Vorteil von mindestens zwei Punkten hat und mindestens drei Punkte gewinnt (dh, wenn er mindestens "vierzig" erreicht hat)..

Definition von Eingabe und Ausgabe

Unsere Anwendung liest die Partitur aus einer Datei. Ein anderes System überträgt die Informationen in diese Datei. Die erste Zeile dieser Datendatei enthält die Namen der Spieler. Wenn ein Spieler einen Punkt erzielt, wird sein Name am Ende der Datei geschrieben. Eine typische Score-Datei sieht folgendermaßen aus:

 John - Michael John John Michael John Michael John John

Diesen Inhalt finden Sie im input.txt Datei in der Quelle Mappe.

Die Ausgabe unseres Programms schreibt die Partitur Zeile für Zeile auf den Bildschirm. Die Ausgabe sollte sein:

 John - Michael John: 15 - Michael: 0 John: 30 - Michael: 0 John: 30 - Michael: 15 John: 40 - Michael: 15 John: 40 - Michael: 30 Deuce John: Advantage John: Winner

Diese Ausgabe finden Sie auch im output.txt Datei. Wir werden diese Informationen verwenden, um zu überprüfen, ob unser Programm korrekt ist.


Der Abnahmetest

Sie schreiben Abnahmetests zu Beginn eines Projekts / Features / einer Story, wenn Sie eine bestimmte Anforderung eindeutig definieren können. In unserem Fall ruft dieser Test einfach unser in Kürze zu erstellendes Skript mit dem Namen der Eingabedatei als Parameter auf und erwartet, dass die Ausgabe mit der handgeschriebenen Datei aus dem vorherigen Abschnitt identisch ist:

 #! / usr / bin / env sh ### AcceptanceTest.sh ### Funktion testItCanProvideAllTheScores () cd… /tennisGame.sh ./input.txt> ./results.txt diff ./output.txt ./results.txt assertTrue 'Die erwartete Ausgabe unterscheidet sich.' $?  ## Alle Tests aufrufen und ausführen. "… /Shunit2-2.1.6/src/shunit2"

Wir werden unsere Tests im durchführen Quelle / Tests Mappe; deshalb, CD… bringt uns in die Quelle Verzeichnis. Dann versucht es zu rennen tennisGamse.sh, was es noch nicht gibt. Dann ist die diff Befehl vergleicht die beiden Dateien: ./output.txt ist unsere handschriftliche Ausgabe und ./results.txt enthält das Ergebnis unseres Skripts. Endlich, assertTrue prüft den Exit-Wert von diff.

Im Moment gibt unser Test jedoch den folgenden Fehler aus:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores ./acceptanceTest.sh: Zeile 7: tennisGame.sh: Befehl nicht gefunden Diff: ./results.txt: Keine solche Datei oder Verzeichnis ASSERT: Die erwartete Ausgabe unterscheidet sich. Ran 1 Test. FAILED (Ausfälle = 1)

Lassen Sie uns diese Fehler in einen schönen Fehler umwandeln, indem Sie eine leere Datei erstellen tennisGame.sh und mache es ausführbar. Wenn wir jetzt unseren Test ausführen, wird kein Fehler angezeigt:

 ./acceptanceTest.sh testItCanProvideAllTheScores 1,9d0 < John - Michael < John: 15 - Michael: 0 < John: 30 - Michael: 0 < John: 30 - Michael: 15 < John: 40 - Michael: 15 < John: 40 - Michael: 30 < Deuce < John: Advantage < John: Winner ASSERT:Expected output differs. Ran 1 test. FAILED (failures=1)

Implementierung mit TDD

Erstellen Sie eine weitere Datei namens unitTests.sh für unsere Unit-Tests. Wir möchten unser Skript nicht für jeden Test ausführen. Wir möchten nur die Funktionen ausführen, die wir testen. Also werden wir machen tennisGame.sh Führe nur die Funktionen aus, die sich darin befinden werden funktionen.sh:

 #! / usr / bin / env sh ### unitTest.sh ### source… /functions.sh function testItCanProvideFirstPlayersName () assertEquals 'John "getFirstPlayerFrom' John - Michael" ## Alle Tests aufrufen und ausführen. "… /Shunit2-2.1.6/src/shunit2"

Unser erster Test ist einfach. Wir versuchen, den Namen des ersten Spielers abzurufen, wenn eine Zeile zwei durch einen Bindestrich getrennte Namen enthält. Dieser Test wird fehlschlagen, da wir noch keine haben getFirstPlayerFrom Funktion:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: Zeile 8: getFirstPlayerFrom: Befehl nicht gefunden Shunit2: ERROR assertEquals () erfordert zwei oder drei Argumente. 1 gegebenes shunit2: FEHLER 1: Johannes 2: 3: Ran-1-Test. OK

Die Implementierung für getFirstPlayerFromist sehr einfach. Es ist ein regulärer Ausdruck, der durch das Symbol gedrückt wird sed Befehl:

 ### functions.sh ### function getFirstPlayerFrom () echo $ 1 | sed -e 's /-.*//'

Nun ist der Test bestanden:

 $ ./unitTest.sh testItCanProvideFirstPlayersName Test 1 Test. OK

Schreiben wir noch einen Test für den Namen des zweiten Spielers:

 ### unitTest.sh ### […] Funktion testItCanProvideSecondPlayersName () assertEquals 'Michael "getSecondPlayerFrom' John - Michael"

Der Fehlschlag:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: erwartet: aber war: Lief 2 Tests. FAILED (Ausfälle = 1)

Und nun die Funktionsimplementierung, um sie durchzulassen:

 ### functions.sh ### […] function getSecondPlayerFrom () echo $ 1 | sed -e 's /.*-//'

Jetzt haben wir Tests bestanden:

$ ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName Lief 2 Tests. OK

Lassen Sie uns die Dinge beschleunigen

Ab diesem Punkt schreiben wir einen Test und die Implementierung, und ich werde nur erklären, was erwähnt werden muss.

Lassen Sie uns testen, ob wir einen Spieler mit nur einer Punktzahl haben. Der folgende Test wurde hinzugefügt:

 Funktion testItCanGetScoreForAPlayerWithOnlyOneWin () Rangliste = $ 'John - Michael \ nJohn' AssertEquals '1 "getScoreFor' John '" $ Standings "'

Und die Lösung:

 Funktion getScoreFor () player = $ 1-Platzierung = $ 2 totalMatches = $ (echo "$ stings") | grep $ player | wc -l) echo $ (($ totalMatches-1))

Wir verwenden einige Phantasie-Hosen-Anführungszeichen, um die Newline-Sequenz zu übergeben (\ n) innerhalb eines String-Parameters. Dann benutzen wir grep um die Zeilen zu finden, die den Namen des Spielers enthalten, und zählen Sie diese mit Toilette. Schließlich subtrahieren wir eine von dem Ergebnis, um dem Vorhandensein der ersten Zeile entgegenzuwirken (diese enthält nur Daten, die keine Punkte betreffen)..

Jetzt befinden wir uns in der Refaktorierungsphase von TDD.

Ich habe gerade festgestellt, dass der Code tatsächlich für mehr als einen Punkt pro Spieler funktioniert, und wir können unsere Tests entsprechend umgestalten. Ändern Sie die obige Testfunktion wie folgt:

 Funktion testItCanGetScoreForAPlayer () Platzierungen = $ 'John - Michael \ nJohn \ nMichael \ nJohn' assertEquals '2 "getScoreFor' John '" $ stings "'

Die Tests bestehen immer noch. Zeit, um mit unserer Logik fortzufahren:

 Funktion testItCanOutputScoreAsInTennisForFirstPoint () assertEquals 'John: 15 - Michael: 0' '' displayScore 'John' 1 'Michael' 0 '"

Und die Umsetzung:

 Funktion displayScore () wenn ["$ 2" -eq '1']; dann playerOneScore = "15" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Ich überprüfe nur den zweiten Parameter. Das sieht aus, als würde ich betrügen, aber es ist der einfachste Code, um den Test zu bestehen. Das Schreiben eines anderen Tests zwingt uns, mehr Logik hinzuzufügen, aber welchen Test sollten wir als nächstes schreiben?

Es gibt zwei Wege, die wir gehen können. Die Prüfung, ob der zweite Spieler einen Punkt erhält, zwingt uns, einen anderen zu schreiben ob Aussage, aber wir müssen nur eine hinzufügen sonst Aussage, wenn wir den zweiten Punkt des ersten Spielers testen möchten. Letzteres impliziert eine einfachere Implementierung, also versuchen wir das:

 Funktion testItCanOutputScoreAsInTennisForSecondPointFirstPlayer () assertEquals 'John: 30 - Michael: 0' '' '' '' 'displayScore' John '2' Michael '0' "

Und die Umsetzung:

 Funktion displayScore () wenn ["$ 2" -eq '1']; dann playerOneScore = "15" sonst playerOneScore = "30" Fi-Echo "$ 1: $ playerOneScore - $ 3: $ 4"

Das sieht immer noch schummelig aus, funktioniert aber einwandfrei. Fortsetzung für den dritten Punkt:

 Funktion testItCanOutputScoreAsInTennisForTHIRDPointFirstPlayer () assertEquals 'John: 40 - Michael: 0' '' 'displayScore' John '3' Michael '0' "

Die Umsetzung:

Funktion displayScore () wenn ["$ 2" -eq '1']; dann playerOneScore = "15" elif ["$ 2" -eq '2']; dann playerOneScore = "30" sonst playerOneScore = "40" Fi-Echo "$ 1: $ playerOneScore - $ 3: $ 4"

Diese if-elif-else fängt an mich zu ärgern. Ich möchte es ändern, aber lassen Sie uns zuerst unsere Tests überarbeiten. Wir haben drei sehr ähnliche Tests. Schreiben wir sie also in einen einzigen Test, der drei Aussagen macht:

 Funktion testItCanOutputScoreWhenFirstPlayerWinsFirst3Points () assertEquals 'John: 15 - Michael: 0' '' 'displayScore' John '1' Michael '0' 'assertEquals' John: 30 - Michael: 0 '' 'displayScore' John '2' Michael '0' "assertEquals 'John: 40 - Michael: 0'" 'displayScore' John '3' Michael '0' "

Das ist besser, und es geht immer noch. Jetzt erstellen wir einen ähnlichen Test für den zweiten Spieler:

 Funktion testItCanOutputScoreWhenSecondPlayerWinsFirst3Points () assertEquals 'John: 0 - Michael: 15' '' 'displayScore' John '0' Michael '1' 'assertEquals' John: 0 - Michael: 30 '' 'displayScore' John '0' Michael '2' "assertEquals 'John: 0 - Michael: 40'" 'displayScore' John '0' Michael '3' "

Das Ausführen dieser Testergebnisse in einer interessanten Ausgabe:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: erwartet: aber war: ASSERT: erwartet: aber war: ASSERT: erwartet: aber war:

Nun, das war unerwartet. Wir wussten, dass Michael falsche Bewertungen hätte. Die Überraschung ist John. Er sollte 0 nicht 40 haben. Lassen Sie uns das beheben, indem Sie zuerst die if-elif-else Ausdruck:

 Funktion displayScore () wenn ["$ 2" -eq '1']; dann playerOneScore = "15" elif ["$ 2" -eq '2']; dann playerOneScore = "30" elif ["$ 2" -eq '3']; dann playerOneScore = "40" sonst playerOneScore = $ 2 fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Das if-elif-else ist jetzt komplexer, aber wir haben zumindest die Punktzahlen des Johns festgelegt:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: erwartet: aber war: ASSERT: erwartet: aber war: ASSERT: erwartet: aber war:

Nun lassen Sie uns Michael reparieren:

 Funktion displayScore () echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" Funktion convertToTennisScore () if ["$ 1" -eq '1']; dann playerOneScore = "15" elif ["$ 1" -eq '2']; dann playerOneScore = "30" elif ["$ 1" -eq '3']; dann playerOneScore = "40" sonst playerOneScore = $ 1 fi echo $ playerOneScore; 

Das hat gut funktioniert! Jetzt ist es an der Zeit, das Hässliche endlich zu überarbeiten if-elif-else Ausdruck:

 Funktion convertToTennisScore () declare -a scoreMap = ('0 "15" 30 "40') echo $ scoreMap [$ 1];

Wertkarten sind wunderbar! Gehen wir weiter zum Fall "Deuce":

 Funktion testItSayDeuceWhenPlayersAreEqualAndHaveEnoughPoinst () assertEquals 'Deuce' "" displayScore 'John' 3 'Michael' 3 '"

Wir prüfen nach "Deuce", wenn alle Spieler mindestens 40 Punkte erreicht haben.

 Funktion displayScore () wenn [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; dann echo "Deuce" else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi

Jetzt testen wir den Vorteil des ersten Spielers:

 Funktion testItCanOutputAdvantageForFirstPlayer () assertEquals 'John: Advantage' "" displayScore 'John' 4 'Michael' 3 '"

Und um es passieren zu lassen:

 Funktion displayScore () wenn [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; dann echo "Deuce" elif [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -gt $ 4]; dann echo "$ 1: Advantage" else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi

Das ist hässlich if-elif-else wieder, und wir haben auch viele Duplizierungen. Alle unsere Tests bestehen, also lassen wir uns überreden:

 Funktion displayScore () if outOfRegularScore $ 2 $ 4; dann checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi Funktion outOfRegularScore () [$ 1 -gt 2] && [$ 2 -gt 2] Rückgabe?  Funktion checkEquality () if [$ 1 -eq $ 2]; dann echo "Deuce" fi Funktion checkFirstPlayerAdv () if [$ 2 -gt $ 3]; dann echo "$ 1: Advantage" fi

Das wird erstmal funktionieren. Testen wir den Vorteil für den zweiten Spieler:

 Funktion testItCanOutputAdvantageForSecondPlayer () assertEquals 'Michael: Advantage' "" displayScore 'John' 3 'Michael' 4 '"

Und der Code:

 Funktion displayScore () if outOfRegularScore $ 2 $ 4; dann checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 3 $ 4 else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi Funktion checkAdvantage () wenn [$ 2 -gt $ 4]; dann echo "$ 1: Advantage" elif [$ 4 -gt $ 2]; dann echo "$ 3: Advantage" fi

Das funktioniert, aber wir haben einige Duplikate checkAdvantage Funktion. Vereinfachen wir es und nennen es zweimal:

 Funktion displayScore () if outOfRegularScore $ 2 $ 4; dann checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 4 checkAdvantage $ 3 $ 4 $ 2 else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi Funktion checkAdvantage () wenn [$ 2 -gt $ 3]; dann echo "$ 1: Advantage" fi

Dies ist tatsächlich besser als unsere bisherige Lösung und es wird auf die ursprüngliche Implementierung dieser Methode zurückgegriffen. Aber wir haben jetzt ein anderes Problem: Ich fühle mich unwohl mit dem 1 US-Dollar, 2 $, 3 $ und 4 $ Variablen. Sie brauchen sinnvolle Namen:

 Funktion displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = $ 4 wenn outOfRegularScore $ firstPlayerScore $ secondPlayerScore; dann checkEquality $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ firstPlayerName $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ secondPlayerName $ secondPlayerScore $ firstPlayerScore sonst echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4' ' ]; dann echo "$ 1: Advantage" fi

Dies macht unseren Code länger, aber er ist deutlich ausdrucksvoller. ich mag das.

Es ist Zeit, einen Gewinner zu finden:

 Funktion testItCanOutputWinnerForFirstPlayer () assertEquals 'John: Winner' "" displayScore 'John' 5 'Michael' 3 '"

Wir müssen nur das ändern checkAdvantageFor Funktion:

 Funktion checkAdvantageFor () if [$ 2 -gt $ 3]; dann wenn ['expr $ 2 - $ 3' -gt 1]; dann Echo "$ 1: Winner", sonst Echo "$ 1: Advantage" fi fi

Wir sind fast fertig! Als letzten Schritt schreiben wir den Code in tennisGame.sh den Abnahmetest bestehen. Dies wird ein ziemlich einfacher Code sein:

 #! / usr / bin / env sh ### tennisGame.sh ###… /functions.sh playersLine = "head -n 1 $ 1" echo "$ playersLine" firstPlayer = "getFirstPlayerFrom" $ playersLine "" secondPlayer = "getSecondPlayerFrom" $ playersLine "" wholeScoreFileContent = "cat $ 1" totalNoOfLines = "echo" $ wholeScoreFileContent "| wc -l" für currentLine in 'seq 2 $ totalNoOfLines' zuerstPlayerScore = $ (getScoreFor $ firstPlayer "echo \" | whole  

Wir lesen die erste Zeile, um die Namen der beiden Spieler abzurufen, und lesen dann inkrementell die Datei, um die Punktzahl zu berechnen.


Abschließende Gedanken

Shell-Skripte können leicht von wenigen Codezeilen auf einige hundert Zeilen anwachsen. In diesem Fall wird die Wartung immer schwieriger. Die Verwendung von TDD und Unit-Tests kann erheblich dazu beitragen, die Pflege komplexer Skripte zu vereinfachen. Außerdem müssen Sie die komplexen Skripts professioneller erstellen.