Test-Driving Shell-skript

Skrive skript skript er veldig mye som programmering. Noen skript krever lite tid investering; mens andre komplekse skript kan kreve tanke, planlegging og større engasjement. Fra dette perspektiv er det fornuftig å ta en testdrevet tilnærming og enhetsprøve våre skalskript.

For å få mest mulig ut av denne opplæringen må du være kjent med kommandolinjegrensesnittet (CLI); du vil kanskje sjekke ut kommandolinjen er din beste venns veiledning hvis du trenger en oppfriskning. Du trenger også en grunnleggende forståelse av Bash-like shell scripting. Til slutt vil du kanskje bli kjent med testdrevet utvikling (TDD) konseptene og enhetstesting generelt; sørg for å sjekke ut disse testdrevne PHP-veiledningene for å få grunnleggende ideen.


Forbered programmeringsmiljøet

Først trenger du en tekstredigerer for å skrive dine skalskript og enhetstester. Bruk din favoritt!

Vi vil bruke shUnit2 shell-enhetens testramme for å kjøre enhetstestene våre. Den ble designet for, og arbeider med, Bash-lignende skall. shUnit2 er et open source-rammeverk utgitt under GPL-lisensen, og en kopi av rammen er også inkludert i denne opplæringseksemplarkildekoden.

Installere shUnit2 er veldig enkelt; Bare last ned og hent arkivet til et hvilket som helst sted på harddisken din. Det er skrevet i Bash, og som sådan består rammen av bare skriptfiler. Hvis du planlegger å bruke shUnit2 ofte, anbefaler jeg at du plasserer det på et sted i PATH.


Skrive vår første test

For denne opplæringen, trekk shUnit inn i en katalog med samme navn i din kilder mappe (se koden vedlagt denne opplæringen). Lage en tester mappe inni kilder og la til en ny filanrop firstTest.sh.

 #! / usr / bin / env sh ### firstTest.sh ### funksjonstestWeCanWriteTests () assertEquals "det fungerer" "det fungerer" ## Call and Run all Tests. "... /shunit2-2.1.6/src/shunit2"

Gjør testfilen din kjørbar.

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

Nå kan du bare kjøre den og observere utgangen:

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

Det står at vi kjørte en vellykket test. La oss få testen til å feile; endre assertEquals setningen slik at de to strengene ikke er de samme og kjører testen igjen:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: forventet: men var: Ran 1 test. FAILED (feil = 1)

Et tennisspill

Du skriver aksepttester i begynnelsen av et prosjekt / en funksjon / historie når du klart kan definere et bestemt krav.

Nå som vi har et arbeidstestmiljø, la oss skrive et skript som leser en fil, tar beslutninger basert på filens innhold og sender informasjon til skjermen.

Hovedmålet med skriptet er å vise poengsummen til et tennisspill mellom to spillere. Vi vil konsentrere oss bare om å holde poengsummen til et enkelt spill; alt annet er opp til deg. Scoring regler er:

  • I begynnelsen har hver spiller en score på null, kalt "kjærlighet"
  • Første, andre og tredje baller vunnet er merket som "femten", "tretti" og "førti".
  • Hvis ved "førti" poengsummen er lik, kalles den "deuce".
  • Etter dette blir poengsummen holdt som "Advantage" for spilleren som scorer et poeng enn den andre spilleren.
  • En spiller er vinneren dersom han klarer å ha en fordel av minst to poeng og vinner minst tre poeng (det vil si hvis han nådde minst "førti").

Definisjon av Input og Output

Vår søknad vil lese poengsummen fra en fil. Et annet system vil skyve informasjonen inn i denne filen. Den første linjen i denne datafilen vil inneholde navnene på spillerne. Når en spiller score et poeng, er navnet sitt skrevet på slutten av filen. En typisk scorefil ser slik ut:

 John - Michael John John Michael John Michael Michael John John

Du kan finne dette innholdet i input.txt fil i Kilde mappe.

Utgangen av vårt program skriver poengsummen til skjermen én linje av gangen. Utgangen skal være:

 John - Michael John: 15 - Michael: 0 John: 30 - Michael: 0 John: 30 - Michael: 15 John: 40 - Michael: 15 John: 40 - Michael: 30 Deuce John: Fordeler John: Vinneren

Denne utgangen kan også finnes i output.txt fil. Vi bruker denne informasjonen for å sjekke om programmet er riktig.


Godkjenningstesten

Du skriver aksepttester i begynnelsen av et prosjekt / en funksjon / historie når du klart kan definere et bestemt krav. I vårt tilfelle kaller denne testen bare vårt snart opprettede skript med navnet på inngangsfilen som parameter, og det forventer at utgangen skal være identisk med den håndskrevne filen fra forrige seksjon:

 #! / usr / bin / env sh ### acceptanceTest.sh ### funksjonstestItCanProvideAllTheScores () cd ... /tennisGame.sh ./input.txt> ./results.txt diff ./output.txt ./results.txt assertTrue Forventet utgang er forskjellig. $?  ## Ring og kjør alle testene. "... /shunit2-2.1.6/src/shunit2"

Vi vil kjøre testene våre i Kilde / Tester mappe; derfor, cd ... tar oss inn i Kilde katalogen. Så prøver den å løpe tennisGamse.sh, som ikke eksisterer ennå. Og så diff kommandoen vil sammenligne de to filene: ./output.txt er vår håndskrevne produksjon og ./results.txt vil inneholde resultatet av vårt skript. Endelig, assertTrue sjekker utgangsverdien av diff.

Men for nå returnerer testen vår følgende feil:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores ./acceptanceTest.sh: linje 7: tennisGame.sh: kommando ikke funnet diff: ./results.txt: Ingen slik fil eller katalog ASSERT: Forventet utgang er forskjellig. Ran 1 test. FAILED (feil = 1)

La oss slå disse feilene til en fin feil ved å opprette en tom fil som heter tennisGame.sh og gjør det kjørbart. Nå når vi kjører testen, får vi ikke en feil:

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

Opprett en annen fil som heter unitTests.sh for våre enhetstester. Vi ønsker ikke å kjøre skriptet vårt for hver test; vi vil bare kjøre de funksjonene vi tester. Så, vi skal gjøre tennisGame.sh Kjør bare funksjonene som skal ligge i functions.sh:

 #! / usr / bin / env sh ### unitTest.sh ### source ... /functions.sh funksjon testItCanProvideFirstPlayersName () assertEquals 'John "getFirstPlayerFrom' John - Michael ' ## Ring og kjør alle test. "... /shunit2-2.1.6/src/shunit2"

Vår første test er enkel. Vi forsøker å hente den første spillers navn når en linje inneholder to navn som er adskilt av en bindestrek. Denne testen mislykkes fordi vi ikke har en getFirstPlayerFrom funksjon:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: linje 8: getFirstPlayerFrom: kommando ikke funnet shunit2: ERROR assertEquals () krever to eller tre argumenter; 1 gitt shunit2: FEIL 1: John 2: 3: Ran 1 test. OK

Gjennomførelsen for getFirstPlayerFromer veldig enkelt. Det er et vanlig uttrykk som skyves gjennom sed kommando:

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

Nå går testen:

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

La oss skrive en annen test for den andre spillerenes navn:

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

Feilen:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: forventet: men var: Ran 2 tester. FAILED (feil = 1)

Og nå er funksjonen implementering for å gjøre det passere:

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

Nå har vi bestått test:

$ ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName Ran 2 tester. OK

La oss få fart på ting

Fra dette tidspunktet vil vi skrive en test og gjennomføringen, og jeg vil bare forklare hva som fortjener å bli nevnt.

La oss teste om vi har en spiller med bare en poengsum. Lagt til følgende test:

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

Og løsningen:

 funksjon getScoreFor () spiller = $ 1 stillinger = $ 2 totalMatches = $ (ekko "$ standings" | grep $ player | wc -l) echo $ (($ totalMatches-1))

Vi bruker noen fancy-bukser citerer for å passere newline sekvensen (\ n) inne i en strengparameter. Så bruker vi grep å finne linjene som inneholder spillerens navn og telle dem med toalett. Til slutt trekker vi en fra resultatet for å motvirke tilstedeværelsen av den første linjen (den inneholder bare ikke-score-relaterte data).

Nå er vi på refactoring-fasen av TDD.

Jeg skjønte bare at koden faktisk fungerer for mer enn ett poeng per spiller, og vi kan refactor våre tester for å gjenspeile dette. Endre testfunksjonen ovenfor til følgende:

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

Testen går fortsatt. Tid til å fortsette med vår logikk:

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

Og gjennomføringen:

 funksjon displayScore () hvis ["$ 2" -eq '1']; da spillerOneScore = "15" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Jeg sjekker bare den andre parameteren. Dette ser ut som jeg jukser, men det er den enkleste koden for å gjøre testen pass. Skrive en annen test tvinger oss til å legge til mer logikk, men hvilken test skal vi skrive neste?

Det er to veier vi kan ta. Testing om den andre spilleren mottar et punkt tvinger oss til å skrive en annen hvis uttalelse, men vi må bare legge til en ellers uttalelse hvis vi velger å teste den første spillers andre poeng. Sistnevnte innebærer en lettere implementering, så la oss prøve det:

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

Og gjennomføringen:

 funksjon displayScore () hvis ["$ 2" -eq '1']; da spillerOneScore = "15" annet spillerOneScore = "30" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Dette ser fortsatt ut, men det fungerer perfekt. Fortsetter videre til det tredje punktet:

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

Implementeringen:

funksjon displayScore () hvis ["$ 2" -eq '1']; så spillerOneScore = "15" elif ["$ 2" -eq '2']; da spillerOneScore = "30" annet spillerOneScore = "40" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Dette if-elif-annet begynner å irritere meg. Jeg vil endre det, men la oss først refactor våre tester. Vi har tre svært like tester; så la oss skrive dem inn i en enkelt test som gjør tre påstander:

 FunksjonstestItCanOutputScoreWhenFirstPlayerWinsFirst3Points () 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' "

Det er bedre, og det går fortsatt. La oss nå lage en lignende test for den andre spilleren:

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

Kjører denne testen resulterer i interessant utgang:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: forventet: men var: Hevde: forventet: men var: Hevde: forventet: men var:

Vel det var uventet. Vi visste at Michael ville ha feil score. Overraskelsen er John; han burde ha 0 ikke 40. La oss fikse det ved først å endre if-elif-annet uttrykk:

 funksjon displayScore () hvis ["$ 2" -eq '1']; så spillerOneScore = "15" elif ["$ 2" -eq '2']; så spillerOneScore = "30" elif ["$ 2" -eq '3']; da spillerOneScore = "40" annet spillerOneScore = $ 2 fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

De if-elif-annet er nå mer komplisert, men vi fastsatte jo jo minst John's score:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: forventet: men var: Hevde: forventet: men var: Hevde: forventet: men var:

La oss nå fikse Michael:

 funksjonen displayScore () echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" funksjon convertToTennisScore () hvis ["$ 1" -eq '1']; så spillerOneScore = "15" elif ["$ 1" -eq '2']; så spillerOneScore = "30" elif ["$ 1" -eq '3']; da playerOneScore = "40" else playerOneScore = $ 1 fi echo $ playerOneScore; 

Det fungerte bra! Nå er det på tide å endelig refactor som stygg if-elif-annet uttrykk:

 funksjon convertToTennisScore () erklære-en scoreMap = ('0 "15" 30 "40') ekko $ scoreMap [$ 1];

Verdi kart er fantastisk! La oss gå videre til "Deuce" -saken:

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

Vi ser etter "Deuce" når alle spillere har minst en score på 40.

 funksjon displayScore () hvis [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; så ekko "Deuce" annet ekko "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi

Nå tester vi for den første spillers fordel:

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

Og for å få det til å passere:

 funksjon displayScore () hvis [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; deretter ekko "Deuce" elif [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -gt $ 4]; deretter ekko "$ 1: Advantage" ellers ekko "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi

Det er så styggt if-elif-annet igjen, og vi har også mye duplisering. Alle våre tester passerer, så la oss refactor:

 funksjon displayScore () hvis outOfRegularScore $ 2 $ 4; deretter checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 andre echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi funksjon outOfRegularScore () [$ 1 -gt 2] && [$ 2 -gt 2] returnere $?  funksjonskontrollEquality () hvis [$ 1 -eq $ 2]; deretter ekko "Deuce" fi funksjonskontrollFirstPlayerAdv () hvis [$ 2 -gt $ 3]; da ekko "$ 1: Advantage" fi

Dette vil fungere for nå. La oss teste fordelen til den andre spilleren:

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

Og koden:

 funksjon displayScore () hvis outOfRegularScore $ 2 $ 4; Sjekk deretter $ 1 $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ deretter ekko "$ 1: Advantage" elif [$ 4 -gt $ 2]; deretter ekko "$ 3: Advantage" fi

Dette fungerer, men vi har litt duplisering i checkAdvantage funksjon. La oss forenkle det og ringe det to ganger:

 funksjon displayScore () hvis outOfRegularScore $ 2 $ 4; Sjekk deretter $ 1 $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ da ekko "$ 1: Advantage" fi

Dette er faktisk bedre enn vår tidligere løsning, og den går tilbake til den opprinnelige implementeringen av denne metoden. Men vi har nå et annet problem: Jeg føler meg ubehagelig med $ 1, $ 2, $ 3 og $ 4 variabler. De trenger meningsfulle navn:

 funksjon displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = $ 4 hvis outOfRegularScore $ firstPlayerScore $ secondPlayerScore; deretter checkEquality $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ firstPlayerName $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ secondPlayerName $ secondPlayerScore $ firstPlayerScore annet ekko "$ 1:" convertToTennisScore $ 2 '- $ 3:' convertToTennisScore $ 4 '"fi funksjonskontrollAdvantageFor () hvis [$ 2 -gt $ 3 ]; da ekko "$ 1: Advantage" fi

Dette gjør vår kode lengre, men den er betydelig mer uttrykksdyktig. jeg liker det.

Det er på tide å finne en vinner:

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

Vi må bare modifisere checkAdvantageFor funksjon:

 funksjonskontrollAdvantageFor () hvis [$ 2 -gt $ 3]; så hvis ['expr $ 2 - $ 3' -gt 1]; da ekko "$ 1: Winner" annet ekko "$ 1: Advantage" fi fi

Vi er nesten ferdige! Som vårt siste skritt, skriver vi koden i tennisGame.sh for å gjøre godkjenningstesten bestått. Dette vil være ganske enkelt kode:

 #! / usr / bin / env sh ### tennisgame.sh ### ... /functions.sh playersLine = "head -n 1 $ 1" ekko "$ playersLine" firstPlayer = "getFirstPlayerFrom" $ playersLine "" secondPlayer = "getSecondPlayerFrom" $ playersLine "" fullScoreFileContent = "cat $ 1" totalNoOfLines = "echo" $ wholeScoreFileContent "| wc -l" for currentLine i 'seq 2 $ totalNoOfLines' gjør firstPlayerScore = $ (getScoreFor $ firstPlayer "'echo \" $ wholeScoreFileContent \ "| head -n $ currentLine ')) secondPlayerScore = $ (getScoreFor $ secondPlayer "' echo \" $ wholeScoreFileContent \ "| head -n $ currentLine ')) VisScore $ firstPlayer $ firstPlayerScore $ secondPlayer $ secondPlayerScore ferdig

Vi leser den første linjen for å hente navnene til de to spillerne, og så leser vi trinnvis filen for å beregne poengsummen.


Siste tanker

Shell-skript kan enkelt vokse fra noen få linjer med kode til noen få hundre linjer. Når dette skjer, blir vedlikeholdet stadig vanskeligere. Bruk av TDD og enhetstesting kan i stor grad bidra til å gjøre det kompliserte skriptet enklere å vedlikeholde - for ikke å nevne at det tvinger deg til å bygge dine komplekse skript på en mer profesjonell måte.