Test-Driving Shell-scripts

Het schrijven van shellscripts lijkt veel op programmeren. Sommige scripts vergen weinig tijdinvestering; overwegende dat andere complexe scripts mogelijk een gedachte, planning en een grotere betrokkenheid vereisen. Vanuit dit perspectief is het logisch om een ​​testgestuurde aanpak te volgen en onze shellscripts te testen.

Om het beste uit deze zelfstudie te halen, moet u bekend zijn met de opdrachtregelinterface (CLI); Misschien wil je de tutorial 'De commandoregel is je beste vriend' eens bekijken als je een opfriscursus nodig hebt. Je hebt ook een basiskennis nodig van Bash-achtige shell-scripting. Ten slotte wilt u misschien bekend raken met de testgestuurde ontwikkeling (TDD) concepten en het testen van eenheden in het algemeen; Zorg ervoor dat je deze PHP-zelfstudies met teststuring bekijkt om het basisidee te krijgen.


Bereid de programmeeromgeving voor

Ten eerste hebt u een teksteditor nodig om uw shellscripts en unit-tests te schrijven. Gebruik je favoriet!

We zullen het shUnit2 shell unit testing framework gebruiken om onze unit tests uit te voeren. Het is ontworpen voor en werkt met bas-achtige schelpen. shUnit2 is een open source framework uitgebracht onder de GPL-licentie, en een kopie van het framework is ook opgenomen in de broncode van deze tutorial.

Het installeren van shUnit2 is heel eenvoudig; download en extraheer het archief eenvoudig naar elke locatie op uw harde schijf. Het is geschreven in Bash en als zodanig bestaat het framework uit alleen scriptbestanden. Als je van plan bent om vaak ShUnit2 te gebruiken, raad ik je ten zeerste aan om het op een locatie in je PATH te plaatsen.


Onze eerste test schrijven

Voor deze zelfstudie, shUnit uitpakken in een map met dezelfde naam in uw bronnen map (zie de code die bij deze zelfstudie hoort). Maak een Tests map binnen bronnen en een nieuwe bestandsoproep toegevoegd firstTest.sh.

 #! / usr / bin / env sh ### firstTest.sh ### functie testWeCanWriteTests () assertEquals "het werkt" "het werkt" ## Bel en voer alle testen uit. "... /shunit2-2.1.6/src/shunit2"

Maak vervolgens uw testbestand uitvoerbaar.

$ cd __uw_code_folder __ / Test $ chmod + x firstTest.sh

Nu kunt u het gewoon uitvoeren en de uitvoer bekijken:

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

Er staat dat we een succesvolle test hebben uitgevoerd. Laten we nu de test laten mislukken; verander de assertEquals zodat de twee strings niet hetzelfde zijn en voer de test opnieuw uit:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: verwacht: maar was: Ran 1-test. FAILED (mislukkingen = 1)

Een tenniswedstrijd

U schrijft acceptatietests aan het begin van een project / kenmerk / verhaal wanneer u een specifieke vereiste duidelijk kunt definiëren.

Nu we een werkende testomgeving hebben, laten we een script schrijven dat een bestand leest, beslissingen neemt op basis van de inhoud van het bestand en informatie naar het scherm uitvoert.

Het belangrijkste doel van het script is om de score van een tennisspel tussen twee spelers te tonen. We zullen ons alleen concentreren op het bijhouden van de score van een enkel spel; al het andere is aan jou. De scoreregels zijn:

  • In het begin heeft elke speler een score van nul, genaamd "love"
  • Eerste, tweede en derde gewonnen ballen worden gemarkeerd als "vijftien", "dertig" en "veertig".
  • Als op "veertig" de score gelijk is, wordt deze "deuce" genoemd.
  • Hierna wordt de score als "Voordeel" behouden voor de speler die nog een punt scoort dan de andere speler.
  • Een speler is de winnaar als hij erin slaagt een voordeel van ten minste twee punten te behalen en wint ten minste drie punten (dat wil zeggen, als hij ten minste "veertig" heeft bereikt).

Definitie van invoer en uitvoer

Onze applicatie leest de score van een bestand. Een ander systeem zal de informatie in dit bestand pushen. De eerste regel van dit gegevensbestand bevat de namen van de spelers. Wanneer een speler een punt scoort, staat zijn naam aan het einde van het bestand. Een typisch score-bestand ziet er als volgt uit:

 John - Michael John John Michael John Michael Michael John John

U kunt deze inhoud vinden in de input.txt bestand in de Bron map.

De uitvoer van ons programma schrijft de score regel voor regel naar het scherm. De uitvoer moet zijn:

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

Deze uitvoer is ook te vinden in de output.txt het dossier. We zullen deze informatie gebruiken om te controleren of ons programma correct is.


De acceptatietest

U schrijft acceptatietests aan het begin van een project / kenmerk / verhaal wanneer u een specifieke vereiste duidelijk kunt definiëren. In ons geval roept deze test eenvoudig ons binnenkort te maken script op met de naam van het invoerbestand als parameter, en het verwacht dat de uitvoer identiek is aan het handgeschreven bestand uit het vorige gedeelte:

 #! / usr / bin / env sh ### acceptanceTest.sh ### function testItCanProvideAllTheScores () cd ... /tennisGame.sh ./input.txt> ./results.txt diff ./output.txt ./results.txt assertTrue 'Verwachte output verschilt.' $?  ## Bel en voer alle tests uit. "... /shunit2-2.1.6/src/shunit2"

We zullen onze tests uitvoeren in de Bron / Tests map; daarom, CD… neemt ons mee naar de Bron directory. Vervolgens probeert het uit te voeren tennisGamse.sh, wat nog niet bestaat. Dan de diff commando vergelijkt de twee bestanden: ./output.txt is onze handgeschreven uitvoer en ./results.txt bevat het resultaat van ons script. Tenslotte, assertTrue controleert de eindwaarde van diff.

Maar voor nu retourneert onze test de volgende fout:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores ./acceptanceTest.sh: regel 7: tennisGame.sh: opdracht niet gevonden diff: ./results.txt: geen bestand of directory ASSERT: verwachte uitvoer verschilt. Ran 1-test. FAILED (mislukkingen = 1)

Laten we van die fouten een leuke fout maken door een leeg bestand te maken met de naam tennisGame.sh en maak het uitvoerbaar. Wanneer we onze test uitvoeren, krijgen we geen foutmelding:

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

Implementatie met TDD

Maak nog een bestand met de naam unitTests.sh voor onze unit tests. We willen ons script niet voor elke test uitvoeren; we willen alleen de functies uitvoeren die we testen. Dus we zullen maken tennisGame.sh voer alleen de functies uit waarin deze zich bevinden functions.sh:

 #! / usr / bin / env sh ### unitTest.sh ### source ... /functions.sh function testItCanProvideFirstPlayersName () assertEquals 'John "getFirstPlayerFrom' John - Michael" ## Bel en voer alle tests uit. "... /shunit2-2.1.6/src/shunit2"

Onze eerste test is eenvoudig. We proberen de naam van de eerste speler te achterhalen wanneer een regel twee namen bevat, gescheiden door een koppelteken. Deze test mislukt omdat we nog geen hebben getFirstPlayerFrom functie:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: regel 8: getFirstPlayerFrom: opdracht niet gevonden shunit2: ERROR assertEquals () vereist twee of drie argumenten; 1 gegeven shunit2: ERROR 1: John 2: 3: Ran 1-test. OK

De implementatie voor getFirstPlayerFromis heel eenvoudig. Het is een reguliere expressie die door de. Wordt geduwd sed commando:

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

Nu passeert de test:

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

Laten we nog een test schrijven voor de naam van de tweede speler:

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

De mislukking:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: verwacht: maar was: Ran 2-tests. FAILED (mislukkingen = 1)

En nu de functie-implementatie om het te laten slagen:

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

Nu hebben we tests afgelegd:

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

Laten we dingen versnellen

Vanaf dit punt zullen we een test en de implementatie schrijven, en ik zal alleen uitleggen wat het verdient om genoemd te worden.

Laten we testen of we een speler hebben met slechts één score. De volgende test toegevoegd:

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

En de oplossing:

 functie getScoreFor () player = $ 1 stand = $ 2 totalMatches = $ (echo "$ standings" | grep $ player | wc -l) echo $ (($ totalMatches-1))

We gebruiken enkele fancy-broeken met aanhalingstekens om de newline-reeks te passeren (\ n) binnen een stringparameter. Dan gebruiken we grep om de regels te vinden die de naam van de speler bevatten en tel ze mee wc. Ten slotte trekken we er een af ​​van het resultaat om de aanwezigheid van de eerste regel tegen te gaan (het bevat alleen niet-scoregegevens).

Nu zijn we in de refactoringfase van TDD.

Ik realiseerde me net dat de code eigenlijk voor meer dan één punt per speler werkt, en we kunnen onze tests refactoren om dit weer te geven. Wijzig de bovenstaande testfunctie naar het volgende:

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

De tests gaan nog steeds voorbij. Tijd om verder te gaan met onze logica:

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

En de implementatie:

 function displayScore () if ["$ 2" -eq '1']; then spelerOneScore = "15" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Ik controleer alleen de tweede parameter. Dit lijkt erop dat ik valsspeel, maar het is de eenvoudigste code om de test te laten slagen. Het schrijven van een andere test dwingt ons om meer logica toe te voegen, maar welke test moeten we daarna schrijven??

Er zijn twee paden die we kunnen nemen. Testen of de tweede speler een punt krijgt, dwingt ons een ander te schrijven als verklaring, maar we hoeven alleen maar een toe te voegen anders verklaring als we ervoor kiezen om het tweede punt van de eerste speler te testen. Dit laatste impliceert een eenvoudiger implementatie, dus laten we dat eens proberen:

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

En de implementatie:

 function displayScore () if ["$ 2" -eq '1']; then playerOneScore = "15" else playerOneScore = "30" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Dit ziet er nog steeds vreemd uit, maar het werkt perfect. Verdergaand op het derde punt:

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

De implementatie:

function displayScore () if ["$ 2" -eq '1']; then playerOneScore = "15" elif ["$ 2" -eq '2']; then playerOneScore = "30" else playerOneScore = "40" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Deze if-elif-else begint me te irriteren. Ik wil het veranderen, maar laten we eerst onze testen refactoren. We hebben drie zeer vergelijkbare tests; dus laten we ze in een enkele test schrijven die drie beweringen doet:

 function 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' "

Dat is beter, en het gaat nog steeds over. Laten we nu een soortgelijke test maken voor de tweede speler:

 function 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' "

Het uitvoeren van deze test resulteert in interessante resultaten:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: verwacht: maar was: STELLEN: verwacht: maar was: STELLEN: verwacht: maar was:

Nou dat was onverwacht. We wisten dat Michael onjuiste scores zou hebben. De verrassing is John; hij zou 0 niet 40 moeten hebben. Laten we dat oplossen door eerst het if-elif-else uitdrukking:

 function displayScore () if ["$ 2" -eq '1']; then playerOneScore = "15" elif ["$ 2" -eq '2']; then playerOneScore = "30" elif ["$ 2" -eq '3']; then playerOneScore = "40" else playerOneScore = $ 2 fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

De if-elif-else is nu complexer, maar we hebben in elk geval de scores van de John gerepareerd:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: verwacht: maar was: STELLEN: verwacht: maar was: STELLEN: verwacht: maar was:

Laten we nu Michael repareren:

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

Dat werkte goed! Nu is het tijd om dat lelijke eindelijk te refactoren if-elif-else uitdrukking:

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

Waardekaarten zijn geweldig! Laten we verder gaan met de zaak "Deuce":

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

We controleren op "Deuce" wanneer alle spelers minstens een score van 40 hebben.

 function displayScore () if [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; dan echo "Deuce" anders echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi

Nu testen we het voordeel van de eerste speler:

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

En om het te laten slagen:

 function displayScore () if [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; dan echo "Deuce" elif [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -gt $ 4]; dan echo "$ 1: Advantage" anders echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi

Daar is dat lelijk if-elif-else nogmaals, en we hebben ook veel duplicatie. Al onze tests verlopen, dus laten we refactoren:

 function displayScore () if outOfRegularScore $ 2 $ 4; checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 anders echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi function outOfRegularScore () [$ 1 -gt 2] && [$ 2 -gt 2] return $?  function checkEquality () if [$ 1 -eq $ 2]; vervolgens echo "Deuce" fi checkFirstPlayerAdv () if [$ 2 -gt $ 3]; dan echo "$ 1: Advantage" fi

Dit werkt voor nu. Laten we het voordeel voor de tweede speler testen:

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

En de code:

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

Dit werkt, maar we hebben wat duplicatie in de checkAdvantage functie. Laten we het vereenvoudigen en het twee keer noemen:

 function displayScore () if outOfRegularScore $ 2 $ 4; checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 4 checkAdvantage $ 3 $ 4 $ 2 anders echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi function checkAdvantage () if [$ 2 -gt $ 3]; dan echo "$ 1: Advantage" fi

Dit is eigenlijk beter dan onze vorige oplossing en het gaat terug naar de oorspronkelijke implementatie van deze methode. Maar we hebben nu een ander probleem: ik voel me ongemakkelijk bij de $ 1, $ 2, $ 3 en $ 4 variabelen. Ze hebben zinvolle namen nodig:

 function displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = $ 4 if outOfRegularScore $ firstPlayerScore $ secondPlayerScore; checkEquality $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ firstPlayerName $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ secondPlayerName $ secondPlayerScore $ firstPlayerScore else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi function checkAdvantageFor () if [$ 2 -gt $ 3 ]; dan echo "$ 1: Advantage" fi

Dit maakt onze code langer, maar het is beduidend expressiever. ik vind het leuk.

Het is tijd om een ​​winnaar te vinden:

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

We hoeven alleen de checkAdvantageFor functie:

 function checkAdvantageFor () if [$ 2 -gt $ 3]; dan als ['expr $ 2 - $ 3' -gt 1]; dan echo "$ 1: Winnaar" anders echo "$ 1: Voordeel" fi fi

We zijn bijna klaar! Als laatste stap schrijven we de code in tennisGame.sh om de acceptatietest te laten slagen. Dit zal een vrij eenvoudige code zijn:

 #! / 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" voor currentLine in 'seq 2 $ totalNoOfLines' do firstPlayerScore = $ (getScoreFor $ firstPlayer "'echo \" $ wholeScoreFileContent \ "| head -n $ currentLine '") secondPlayerScore = $ (getScoreFor $ secondPlayer"' echo \ "$ wholeScoreFileContent \" | head -n $ currentLine '") displayScore $ firstPlayer $ firstPlayerScore $ secondPlayer $ secondPlayerScore done

We lezen de eerste regel om de namen van de twee spelers op te halen en vervolgens lezen we stapsgewijs het bestand om de score te berekenen.


Laatste gedachten

Shell-scripts kunnen eenvoudig groeien van enkele regels code tot een paar honderd regels. Wanneer dit gebeurt, wordt onderhoud steeds moeilijker. Het gebruik van TDD en testen van eenheden kan enorm helpen om je complexe script gemakkelijker te onderhouden te maken - om nog maar te zwijgen van het feit dat het je dwingt om je complexe scripts op een meer professionele manier te bouwen.