Test-Driving Shell Scripts

Skrivande skalskript är väldigt mycket som programmering. Vissa skript kräver liten investeringstid; medan andra komplexa skript kan kräva tanke, planering och ett större engagemang. Ur det här perspektivet är det meningsfullt att ta ett testdriven tillvägagångssätt och enhetstesta våra skalskript.

För att få ut det mesta av denna handledning måste du vara bekant med kommandoradsgränssnittet (CLI); du kanske vill kolla upp kommandoraden är din bästa vänhandledning om du behöver en uppdatering. Du behöver också en grundläggande förståelse för Bash-liknande skalskription. Slutligen kanske du vill bekanta dig med testdriven utveckling (TDD) -koncept och enhetstestning i allmänhet. var noga med att kolla in dessa testdrivna PHP-handledning för att få den grundläggande idén.


Förbered programmeringsmiljön

Först behöver du en textredigerare för att skriva dina skalskript och enhetstester. Använd din favorit!

Vi använder shUnit2-skalenhetens testramverk för att köra våra enhetstester. Den var designad för, och fungerar med, Bash-liknande skal. shUnit2 är en öppen källkodsram som släpps ut under GPL-licensen, och en kopia av ramverket ingår också i den här handledningens provkodskod.

Installera shUnit2 är väldigt enkelt; helt enkelt ladda ner och extrahera arkivet till någon plats på din hårddisk. Det är skrivet i Bash, och som sådan består ramverket av endast skriptfiler. Om du planerar att använda ShUnit2 ofta, rekommenderar jag starkt att du placerar den på en plats i din PATH.


Skriva vårt första test

För denna handledning extraheras shUnit till en katalog med samma namn i din källor mapp (se koden som bifogas denna handledning). Skapa en tester mapp inuti källor och lagt till ett nytt filsamtal firstTest.sh.

 #! / usr / bin / env sh ### firstTest.sh ### funktionstestWeCanWriteTests () assertEquals "det fungerar" "det fungerar" ## Ring och kör alla test. "... /shunit2-2.1.6/src/shunit2"

Än gör din testfil körbar.

$ cd __your_code_folder __ / Tests $ chmod + x firstTest.sh

Nu kan du helt enkelt köra den och observera utgången:

 $ ./firstTest.sh testWeCanWriteTests Ran 1 test. ok

Det säger att vi körde ett framgångsrikt test. Nu, låt oss få testet att misslyckas; ändra assertEquals uttalande så att de två strängarna inte är samma och kör testet igen:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: förväntat: men var: Ran 1 test. FAILED (fel = 1)

Ett tennisspel

Du skriver acceptanstest i början av ett projekt / en funktion / en historia när du tydligt kan definiera ett specifikt krav.

Nu när vi har en arbetsprovningsmiljö, låt oss skriva ett skript som läser en fil, tar beslut baserat på filens innehåll och matar ut information till skärmen.

Huvudmålet med manuset är att visa poängen för ett tennisspel mellan två spelare. Vi kommer att koncentrera oss bara på att hålla poängen för ett enda spel; allting är upp till dig. Scoringsreglerna är:

  • I början har varje spelare en poäng på noll, kallad "kärlek"
  • Första, andra och tredje bollen vann är märkta som "femton", "trettio" och "fyrtio".
  • Om vid "fyrtio" poängen är lika kallas den "deuce".
  • Därefter hålls poängen som "Advantage" för den spelare som poängerar en poäng än den andra spelaren.
  • En spelare är vinnaren om han lyckas ha en fördel med minst två poäng och vinner minst tre poäng (det vill säga om han når minst "fyrtio").

Definition av Input och Output

Vår ansökan kommer att läsa poängen från en fil. Ett annat system kommer att trycka informationen i den här filen. Den första raden i den här datafilen innehåller namnen på spelarna. När en spelare får en poäng, skrivs namnet i slutet av filen. En typisk poängfil ser så här ut:

 John - Michael John John Michael John Michael Michael John John

Du kan hitta detta innehåll i input.txt fil i Källa mapp.

Utgången i vårt program skriver poängen till skärmen en rad i taget. Utgången ska vara:

 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: Vinnare

Denna utgång kan också hittas i output.txt fil. Vi använder denna information för att kontrollera om vårt program är korrekt.


Acceptanstestet

Du skriver acceptanstest i början av ett projekt / en funktion / en historia när du tydligt kan definiera ett specifikt krav. I det här fallet kallas det här testet vårt snart-skapade skript med namnet på inmatningsfilen som parameter och det förväntar sig att utmatningen ska vara identisk med den handskrivna filen från föregående avsnitt:

 #! / usr / bin / env sh ### acceptanceTest.sh ### funktionstestItCanProvideAllTheScores () cd ... /tennisGame.sh ./input.txt> ./results.txt diff ./output.txt ./results.txt assertTrue "Förväntad produktion skiljer sig åt." $?  ## Ring och kör alla test. "... /shunit2-2.1.6/src/shunit2"

Vi kör våra test i Source / Tester mapp; därför, CD… tar oss in i Källa katalogen. Då försöker den springa tennisGamse.sh, som ännu inte existerar. Sedan diff Kommando kommer att jämföra de två filerna: ./output.txt är vår handskrivna produktion och ./results.txt kommer att innehålla resultatet av vårt manus. Till sist, assertTrue kontrollerar utgångsvärdet av diff.

Men för närvarande returnerar vårt test följande fel:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores ./acceptanceTest.sh: rad 7: tennisGame.sh: kommandot hittades inte diff: ./results.txt: Ingen sådan fil eller katalog ASSERT: Förväntad utgåva skiljer sig. Ran 1 test. FAILED (fel = 1)

Låt oss göra dessa fel till ett bra fel genom att skapa en tom fil som heter tennisGame.sh och gör det körbart. Nu när vi kör vårt test får vi inte ett fel:

 ./ 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)

Implementering med TDD

Skapa en annan fil som heter unitTests.sh för våra enhetstester. Vi vill inte köra vårt manus för varje test; vi vill bara köra de funktioner som vi testar. Så ska vi göra tennisGame.sh kör endast de funktioner som kommer att ligga i functions.sh:

 #! / usr / bin / env sh ### unitTest.sh ### source ... /functions.sh funktion testItCanProvideFirstPlayersName () assertEquals 'John' getFirstPlayerFrom 'John - Michael' ## Ring och kör alla test. "... /shunit2-2.1.6/src/shunit2"

Vårt första test är enkelt. Vi försöker hämta den första spelarens namn när en rad innehåller två namn separerade av en bindestreck. Detta test kommer att misslyckas eftersom vi ännu inte har en getFirstPlayerFrom fungera:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: linje 8: getFirstPlayerFrom: kommandot hittades inte shunit2: ERROR assertEquals () kräver två eller tre argument; 1 given shunit2: FEL 1: John 2: 3: Ran 1 test. ok

Genomförandet för getFirstPlayerFromär väldigt enkelt. Det är ett vanligt uttryck som drivs genom sed kommando:

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

Nu passerar testet:

 $ ./unitTest.sh testItCanProvideFirstPlayersName Ran 1 test. ok

Låt oss skriva ett annat test för den andra spelarens namn:

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

Misslyckandet:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: förväntat: men var: Ran 2 test. FAILED (fel = 1)

Och nu genomförandet av funktionen för att få det att passera:

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

Nu har vi passande tester:

$ ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName Ran 2 test. ok

Låt oss snabba saker upp

Från och med nu börjar vi skriva ett test och genomförandet, och jag kommer bara att förklara vad som förtjänar att nämnas.

Låt oss testa om vi har en spelare med bara ett poäng. Tillagde följande test:

 funktion testItCanGetScoreForAPlayerWithOnlyOneWin () standings = $ 'John - Michael \ nJohn' assertEquals '1 "getScoreFor' John '" $ standings "'

Och lösningen:

 funktion getScoreFor () spelare = $ 1 ställningar = $ 2 totalMatches = $ (echo "$ standings" | grep $ spelare | wc -l) echo $ (($ totalMatches-1))

Vi använder några fancy-byxor citerar för att passera newline-sekvensen (\ n) inuti en strängparameter. Då använder vi grep för att hitta de linjer som innehåller spelarens namn och räkna dem med toalett. Slutligen subtraherar vi en från resultatet för att motverka närvaron av den första raden (den innehåller endast icke-poängrelaterade data).

Nu är vi på refactoring-fasen av TDD.

Jag insåg precis att koden faktiskt fungerar för mer än en poäng per spelare, och vi kan refactor våra test för att reflektera detta. Ändra ovanstående testfunktion till följande:

 funktion testItCanGetScoreForAPlayer () standings = $ 'John - Michael \ nJohn \ nMichael \ nJohn' assertEquals '2 "getScoreFor' John '" $ standings "'

Testerna passerar fortfarande. Tiden att fortsätta med vår logik:

 funktion testItCanOutputScoreAsInTennisForFirstPoint () assertEquals 'John: 15 - Michael: 0' "'DisplayScore' John '1' Michael '0'"

Och genomförandet:

 funktionen displayScore () om ["$ 2" -eq '1']; då playerOneScore = "15" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Jag kontrollerar bara den andra parametern. Det ser ut som att jag lurar, men det är den enklaste koden för att göra provet. Att skriva ett annat test tvingar oss att lägga till mer logik, men vilket test ska vi skriva nästa??

Det finns två vägar vi kan ta. Testa om den andra spelaren tar emot en poäng tvingar oss att skriva en annan om uttalande, men vi måste bara lägga till en annan uttalande om vi väljer att testa den första spelarens andra punkt. Det senare innebär ett enklare genomförande, så låt oss försöka det:

 funktion testItCanOutputScoreAsInTennisForSecondPointFirstPlayer () assertEquals 'John: 30 - Michael: 0' "'DisplayScore' John '2' Michael '0'"

Och genomförandet:

 funktionen displayScore () om ["$ 2" -eq '1']; då playerOneScore = "15" annars playerOneScore = "30" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Det här ser fortfarande ut som fusk, men det fungerar perfekt. Fortsättning på den tredje punkten:

 funktion testItCanOutputScoreAsInTennisForTHIRDPointFirstPlayer () assertEquals 'John: 40 - Michael: 0' "'DisplayScore' John '3' Michael '0'"

Genomförandet:

funktionen displayScore () om ["$ 2" -eq '1']; då playerOneScore = "15" elif ["$ 2" -eq '2']; då playerOneScore = "30" annars playerOneScore = "40" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Detta if-elif-else börjar irritera mig. Jag vill ändra det, men låt oss först refactor våra tester. Vi har tre mycket liknande tester; så låt oss skriva dem i ett enda test som gör tre påståenden:

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

Det är bättre, och det passerar fortfarande. Låt oss nu skapa ett liknande test för den andra spelaren:

 FunktionstestItCanOutputScoreWhenSecondPlayerWinsFirst3Points () assertEquals 'John: 0 - Michael: 15' "'ShowScore 'John' 0 'Michael' 1 '" assertEquals' John: 0 - Michael: 30'''playScore 'John' 0 'Michael' 2 ' "assertEquals" John: 0 - Michael: 40 '' 'displayScore' John '0' Michael '3' "

Att köra detta test resulterar i intressant resultat:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: förväntat: men var: HÄVDA: förväntat: men var: HÄVDA: förväntat: men var:

Jo det var oväntat. Vi visste att Michael skulle ha felaktiga poäng. Överraskningen är John; han borde ha 0 inte 40. Låt oss fixa det genom att först ändra if-elif-else uttryck:

 funktionen displayScore () om ["$ 2" -eq '1']; då playerOneScore = "15" elif ["$ 2" -eq '2']; sedan playerOneScore = "30" elif ["$ 2" -eq '3']; då playerOneScore = "40" annars playerOneScore = $ 2 fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

De if-elif-else är nu mer komplicerat, men vi fixade åtminstone John: s poäng:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: förväntat: men var: HÄVDA: förväntat: men var: HÄVDA: förväntat: men var:

Nu låt oss fixa Michael:

 funktionen displayScore () echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" funktionen convertToTennisScore () if ["$ 1" -eq '1']; sedan playerOneScore = "15" elif ["$ 1" -eq '2']; då playerOneScore = "30" elif ["$ 1" -eq '3']; då playerOneScore = "40" annars playerOneScore = $ 1 fi echo $ playerOneScore; 

Det fungerade bra! Nu är det dags att slutligen refactor det ful if-elif-else uttryck:

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

Värdekartor är underbara! Låt oss gå vidare till "Deuce" fallet:

 funktionstestItSayDeuceWhenPlayersAreEqualAndHaveEnoughPoinst () assertEquals 'Deuce' '' displayScore 'John' 3 'Michael' 3 '"

Vi söker efter "Deuce" när alla spelare har minst en poäng på 40.

 funktionsdisplayScore () om [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; sedan echo "Deuce" annars echo "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi

Nu testar vi för den första spelarens fördel:

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

Och för att klara det:

 funktionsdisplayScore () om [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; ekko "Deuce" elif [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -gt $ 4]; sedan echo "$ 1: Advantage" annars echo "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi

Det är så fult if-elif-else igen, och vi har också mycket dubbelarbete. Alla våra test passerar, så låt oss refactor:

 funktion displayScore () om outOfRegularScore $ 2 $ 4; sedan checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 annan echo "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi funktion outOfRegularScore () [$ 1 -gt 2] && [$ 2 -gt 2] returnera $?  funktionskontrollEquality () om [$ 1 -eq $ 2]; sedan echo "Deuce" fi funktion checkFirstPlayerAdv () if [$ 2 -gt $ 3]; sedan echo "$ 1: Advantage" fi

Detta kommer att fungera för nu. Låt oss testa fördelen för den andra spelaren:

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

Och koden:

 funktion displayScore () om outOfRegularScore $ 2 $ 4; sedan checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 3 $ 4 annars echo "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi funktion checkAdvantage () om [$ 2 -gt $ 4]; sedan echo "$ 1: Advantage" elif [$ 4 -gt $ 2]; sedan echo "$ 3: Advantage" fi

Det här fungerar, men vi har lite dubbelarbete i checkAdvantage fungera. Låt oss förenkla det och kalla det två gånger:

 funktion displayScore () om outOfRegularScore $ 2 $ 4; sedan checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 4 checkAdvantage $ 3 $ 4 $ 2 annars echo "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi funktion checkAdvantage () om [$ 2 -gt $ 3]; sedan echo "$ 1: Advantage" fi

Detta är faktiskt bättre än vår tidigare lösning, och den återgår till den ursprungliga implementeringen av denna metod. Men vi har nu ett annat problem: Jag känner mig obekväm med $ 1, $ 2, $ 3 och $ 4 variabler. De behöver meningsfulla namn:

 funktionen displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = $ 4 om outOfRegularScore $ firstPlayerScore $ secondPlayerScore; sedan checkEquality $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ firstPlayerName $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ secondPlayerName $ secondPlayerScore $ firstPlayerScore annat echo "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi funktion checkAdvantageFor () om [$ 2 -gt $ 3 ]; sedan echo "$ 1: Advantage" fi

Detta gör vår kod längre, men den är betydligt mer uttrycksfull. jag gillar det.

Det är dags att hitta en vinnare:

 funktion testItCanOutputWinnerForFirstPlayer () assertEquals 'John: Vinnare' "'displayScore' John '5' Michael '3'"

Vi behöver bara ändra checkAdvantageFor fungera:

 funktionskontrollAdvantageFor () om [$ 2 -gt $ 3]; då om ['expr $ 2 - $ 3' -gt 1]; sedan echo "$ 1: Winner" annars echo "$ 1: Advantage" fi fi

Vi är nästan färdiga! Som vårt sista steg skriver vi koden i tennisGame.sh för att göra godkännandetestet. Detta kommer att vara ganska enkelt kod:

 #! / usr / bin / env sh ### tennisspel.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 i 'seq 2 $ totalNoOfLines' gör firstPlayerScore = $ (getScoreFor $ firstPlayer "" echo \ "$ wholeScoreFileContent \" | head $ ns

Vi läser den första raden för att hämta namnen på de två spelarna, och sedan läser vi stegvis filen för att beräkna poängen.


Slutgiltiga tankar

Skallskript kan enkelt växa från några rader av kod till några hundra linjer. När detta händer blir underhållet allt svårare. Att använda TDD och enhetstestning kan i hög grad bidra till att göra ditt komplexa skript enklare att behålla - för att inte tala om att det tvingar dig att bygga dina komplexa skript på ett mer professionellt sätt.