Șabloane Shell Driving Driving

Scrierea scripturilor shell este foarte mult ca și programarea. Unele scripturi necesită puțin timp pentru investiții; în timp ce alte scenarii complexe pot necesita gândire, planificare și un angajament mai mare. Din această perspectivă, este logic să luăm o abordare bazată pe test și unitatea să testeze scripturile noastre de shell.

Pentru a profita la maximum de acest tutorial, trebuie să fiți familiarizați cu interfața liniei de comandă (CLI); poate doriți să verificați linia de comandă este tutorialul cel mai bun prieten dacă aveți nevoie de o reîmprospătare. De asemenea, aveți nevoie de o înțelegere de bază a scripting-ului de tip shell Bash. În cele din urmă, vă recomandăm să vă familiarizați cu conceptele dezvoltării bazate pe test (TDD) și cu testarea unităților în general; asigurați-vă că pentru a verifica aceste teste de conducere PHP-teste pentru a obține ideea de bază.


Pregătiți mediul de programare

În primul rând, aveți nevoie de un editor de text pentru a scrie scripturile shell și testele unității. Utilizați preferatul!

Vom folosi cadrul de testare shUnit2 unit shell pentru a rula testele unitare. A fost proiectat și funcționează cu niște cochilii de tip Bash. shUnit2 este un cadru open source lansat sub licența GPL, iar o copie a cadrului este inclusă și în codul sursă al acestui tutorial.

Instalarea shUnit2 este foarte ușoară; pur și simplu descărcați și extrageți arhiva în orice locație de pe hard disk. Este scris în Bash și, ca atare, cadrul constă doar din fișiere de script. Dacă intenționați să folosiți frecvent shUnit2, vă recomandăm să o puneți într-o locație în PATH.


Scriind primul nostru test

Pentru acest tutorial, extrage SHUnit într-un director cu același nume în dvs. surse (vedeți codul atașat la acest tutorial). Creeaza o teste dosarul din interior surse și a adăugat un nou apel de fișier firstTest.sh.

 #! / usr / bin / env sh ### primaTest.sh ### funcția testWeCanWriteTests () assertEquals "funcționează" "funcționează" ## Apelați și executați toate testele. "... /shunit2-2.1.6/src/shunit2"

Apoi faceti fișierul de test executabil.

$ cd __your_code_folder __ / Testează $ chmod + x firstTest.sh

Acum, puteți să o executați și să observați rezultatul:

 $ ./firstTest.sh testWeCanWriteTests Testul Ran 1. O.K

Se spune că am făcut un test de succes. Acum, hai să facem testul să eșueze; schimba assertEquals astfel încât cele două șiruri de caractere să nu fie identice și să ruleze din nou testul:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: așteptat: dar a fost: Testul de rang 1. FAILED (eșecuri = 1)

Un joc de tenis

Scrieți teste de acceptare la începutul unui proiect / facilitate / poveste atunci când puteți defini în mod clar o cerință specifică.

Acum, că avem un mediu de testare de lucru, să scriem un script care citește un fișier, ia decizii bazate pe conținutul fișierului și transmite informații către ecran.

Scopul principal al scenariului este de a arăta scorul unui joc de tenis între doi jucători. Ne vom concentra doar pe păstrarea scorului unui singur joc; totul depinde de tine. Regulile de punctare sunt:

  • La început, fiecare jucător are un scor de zero, numit "dragoste"
  • Prima, a doua și a treia bile câștigătoare sunt marcate ca "cincisprezece", "treizeci" și "patruzeci".
  • Dacă la "patruzeci" scorul este egal, se numește "deuce".
  • După aceasta, scorul este păstrat ca "Avantaj" pentru jucătorul care scrii încă un punct decât celălalt jucător.
  • Un jucător este câștigătorul dacă reușește să obțină un avantaj de cel puțin două puncte și să câștige cel puțin trei puncte (adică dacă a atins cel puțin patruzeci de puncte).

Definiția intrării și ieșirii

Cererea noastră va citi scorul dintr-un fișier. Un alt sistem va împinge informațiile în acest fișier. Primul rând al acestui fișier de date va conține numele jucătorilor. Când un jucător marchează un punct, numele acestuia este scris la sfârșitul fișierului. Un fișier de scor tipic arată astfel:

 Ioan - Michael John Ioan Michael Ioan Michael Michael Ioan John

Puteți găsi acest conținut în INPUT.TXT fișier în Sursă pliant.

Rezultatul programului nostru scrie scorul la ecranul o singură linie la un moment dat. Rezultatul ar trebui să fie:

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

Această ieșire poate fi găsită și în output.txt fişier. Vom folosi aceste informații pentru a verifica dacă programul nostru este corect.


Testul de acceptare

Scrieți teste de acceptare la începutul unui proiect / facilitate / poveste atunci când puteți defini în mod clar o cerință specifică. În cazul nostru, acest test numește pur și simplu scriptul nostru care urmează să fie creat cu numele fișierului de intrare ca parametru și se așteaptă ca rezultatul să fie identic cu fișierul scris manual din secțiunea anterioară:

 #! / usr / bin / env sh ### acceptareTest.sh ### funcția testItCanProvideAllTheScores () cd ... /tennisGame.sh ./input.txt> ./results.txt diff ./output.txt ./results.txt assertTrue "Puterea estimată diferă." $?  ## Apelează și execută toate testele. "... /shunit2-2.1.6/src/shunit2"

Vom face testele noastre în Sursă / Teste pliant; prin urmare, CD… ne duce în Sursă director. Apoi încearcă să fugă tennisGamse.sh, care nu există încă. Apoi dif comanda va compara cele două fișiere: ./output.txt este producția noastră manuală și ./results.txt va conține rezultatul scriptului nostru. In cele din urma, assertTrue verifică valoarea de ieșire din dif.

Dar pentru moment, testul nostru returnează următoarea eroare:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores./acceptanceTest.sh: linia 7: tennisGame.sh: comanda nu a fost găsită diff: ./results.txt: Nici un astfel de fișier sau director ASSERT: ieșirea așteptată diferă. Testul de rang 1. FAILED (eșecuri = 1)

Să transformăm aceste erori într-un eșec frumos prin crearea unui fișier gol numit tennisGame.sh și să fie executabil. Acum, când conducem testul nostru, nu avem o eroare:

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

Implementarea cu TDD

Creați un alt fișier numit unitTests.sh pentru testele unitare. Nu vrem să rulați scenariul nostru pentru fiecare test; dorim doar să executăm funcțiile pe care le testăm. Deci, vom face tennisGame.sh executați numai funcțiile în care se vor afla functions.sh:

 #! / usr / bin / env sh ### unitateTest.sh ### sursă ... / funcții.sh funcție testItCanProvideFirstPlayersName () assertEquals 'John' getFirstPlayerFrom 'John - Michael " ## Apelați și rulați toate testele. "... /shunit2-2.1.6/src/shunit2"

Primul nostru test este simplu. Încercăm să obținem numele primului jucător atunci când o linie conține două nume separate printr-o cratimă. Acest test nu va reuși deoarece nu avem încă un test getFirstPlayerFrom funcţie:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: linia 8: getFirstPlayerFrom: comanda nu a fost găsită shunit2: ERROR assertEquals () necesită două sau trei argumente; 1 dată shunit2: EROARE 1: Ioan 2: 3: Testul a fost 1. O.K

Implementarea pentru getFirstPlayerFromeste foarte simplu. Este o expresie regulată care este împinsă prin sed comanda:

 ### funcții.sh ### funcție getFirstPlayerFrom () echo $ 1 | sed -e /-.*// '

Acum, testul trece:

 $ ./unitTest.sh testItCanProvideFirstPlayersName Testul Ran 1. O.K

Să scriem un alt test pentru numele celui de-al doilea jucător:

 ### unitTest.sh ### [...] funcția testItCanProvideSecondPlayersName () assertEquals "Michael" getSecondPlayerFrom "John - Michael"

Esecul:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: așteptat: dar a fost: Testele Ran 2. FAILED (eșecuri = 1)

Și acum implementarea funcției pentru ao face să treacă:

 ### funcții.sh ### [...] funcția getSecondPlayerFrom () echo $ 1 | sed-e /.*-// ''

Acum avem teste:

$ ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName Ran 2 teste. O.K

Hai să accelerăm lucrurile

Începând cu acest moment, vom scrie un test și implementarea și voi explica doar ce merită să fie menționat.

Sa testam daca avem un jucator cu un singur scor. A fost adăugat următorul test:

 funcția testItCanGetScoreForAPlayerWithOnlyOneWin () clasificări = $ 'John - Michael \ nJohn' assertEquals '1 "getScoreFor' John '" $ standings "'

Și soluția:

 funcția getScoreFor () player = $ 1 clasă = $ 2 totalMatches = $ (echo "$ clasă" | grep $ player | wc -l) echo $ (($ totalMatches-1)

Folosim niște pantaloni care citează să treacă succesiunea de linie nouă (\ n) în interiorul unui parametru de șir. Apoi vom folosi grep pentru a găsi liniile care conțin numele jucătorului și le numără toaleta. În cele din urmă, scădem unul din rezultat pentru a contracara prezența primei linii (conține doar date care nu sunt legate de scor).

Acum suntem în faza de refactorizare a TDD.

Tocmai mi-am dat seama că codul funcționează de fapt pentru mai mult de un punct pe jucător și putem să refacem testele noastre pentru a reflecta acest lucru. Modificați funcția de testare de mai sus la următoarele:

 funcția testItCanGetScoreForAPlayer () clasificări = $ 'John - Michael \ nJohn \ nMichael \ nJohn' assertEquals '2 "getScoreFor' John '" $ standings "'

Testele continuă să treacă. Este timpul să mergem mai departe cu logica noastră:

 funcția testItCanOutputScoreAsInTennisForFirstPoint () assertEquals 'Ioan: 15 - Michael: 0' '' displayScore 'John' 1 'Michael' 0 '"

Și punerea în aplicare:

 funcția displayScore () if ["$ 2" -eq '1']; apoi playerOneScore = "15" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Verific numai cel de-al doilea parametru. Acest lucru pare ca am inselat, dar este cel mai simplu cod pentru a face testul. Scrierea unui alt test ne obligă să adăugăm mai multă logică, dar ce test trebuie să scriem în continuare?

Există două căi pe care le putem lua. Testarea dacă cel de-al doilea jucător primește un punct ne obligă să scriem altul dacă declarație, dar trebuie doar să adăugăm o altfel dacă alegem să testați al doilea punct al primului jucător. Acesta din urmă implică o implementare mai ușoară, așa că hai să încercăm:

 funcția testItCanOutputScoreAsInTennisForSecondPointFirstPlayer () assertEquals 'Ioan: 30 - Michael: 0' '' displayScore 'John' 2 'Michael' 0 '"

Și punerea în aplicare:

 funcția displayScore () if ["$ 2" -eq '1']; apoi playerOneScore = "15" altceva playerOneScore = "30" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Acest lucru încă arată înșelăciune, dar funcționează perfect. Continuând pentru al treilea punct:

 funcția testItCanOutputScoreAsInTennisForTHIRDPointFirstPlayer () assertEquals 'John: 40 - Michael: 0' '' displayScore 'John' 3 'Michael' 0 '"

Implementarea:

funcția displayScore () if ["$ 2" -eq '1']; apoi playerOneScore = "15" elif ["$ 2" -eq '2']; apoi playerOneScore = "30" altceva playerOneScore = "40" fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

Acest -If-Elif altceva începe să mă enerveze. Vreau să-l schimb, dar mai întâi să refacem testele noastre. Avem trei teste foarte asemănătoare; așa că să le scriem într-un singur test care face trei afirmații:

 () assertEquals 'John: 15 - Michael: 0' '' displayScore 'John' 1 'Michael' 0 '"assertEquals' "AssertEquals" Ioan: 40 - Michael: 0 '' 'displayScore' John '3' Michael '0' "

E mai bine și încă mai trece. Acum, să creăm un test similar pentru al doilea jucător:

 functionare testItCanOutputScoreWhenSecondPlayerWinsFirst3 Puncte () assertEquals 'John: 0 - Michael: 15' '' displayScore 'John' 0 'Michael' 1 '"assertEquals' "assertEquals" Ioan: 0 - Michael: 40 '' 'displayScore' John '0' Michael '3' "

Rularea acestui test are rezultate interesante:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: așteptat: dar a fost: Afirmați: de așteptat: dar a fost: Afirmați: de așteptat: dar a fost:

Asta a fost neașteptat. Știam că Michael ar avea scoruri incorecte. Surpriza este John; el ar trebui să aibă 0 nu 40. Să rezolvăm asta prin modificarea mai întâi a -If-Elif altceva expresie:

 funcția displayScore () if ["$ 2" -eq '1']; apoi playerOneScore = "15" elif ["$ 2" -eq '2']; apoi playerOneScore = "30" elif ["$ 2" -eq '3']; apoi playerOneScore = "40" altceva playerOneScore = $ 2 fi echo "$ 1: $ playerOneScore - $ 3: $ 4"

-If-Elif altceva este acum mai complexă, dar cel puțin am fixat scorurile lui John:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: așteptat: dar a fost: Afirmați: de așteptat: dar a fost: Afirmați: de așteptat: dar a fost:

Acum să rezolvăm Michael:

 () [$ 1 "-eq '1'] funcția displayScore () echo" $ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4' " funcția convertToTennisScore () ; apoi playerOneScore = "15" elif ["$ 1" -eq '2']; apoi playerOneScore = "30" elif ["$ 1" -eq '3']; apoi playerOneScore = "40" altceva playerOneScore = $ 1 ech echo $ playerOneScore; 

Asta a funcționat bine! Acum este timpul să refăcuiți acest lucru urât -If-Elif altceva expresie:

 funcția convertToTennisScore () declare -a scoreMap = ('0' 15 '30' 40 ') echo $ scoreMap [$ 1];

Valorile hărților sunt minunate! Să trecem la cazul "Deuce":

 funcția testItSayDeuceWhenPlayersAreEqualAndHaveEnoughPoinst () assertEquals 'Deuce' "'afișareScore' John '3' Michael '3'"

Verificăm "Deuce" când toți jucătorii au cel puțin un scor de 40.

 funcția displayScore () dacă [$ 2 -gt 2] && [$ 4 -gt2] && [$ 2 -eq $ 4]; apoi echo "Deuce" altceva echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi

Acum încercăm pentru avantajul primului jucător:

 funcția testItCanOutputAdvantageForFirstPlayer () assertEquals 'John: Advantage' "'afișareScore' John '4' Michael '3'"

Și pentru a face trecerea:

 funcția displayScore () dacă [$ 2 -gt 2] && [$ 4 -gt2] && [$ 2 -eq $ 4]; apoi faceți clic pe "Deuce" elif [$ 2 -gt2] && [$ 4 -gt2] && [$ 2 -gt $ 4]; apoi echo "$ 1: Advantage" altceva echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi

E urâtul ăla -If-Elif altceva din nou, și avem o mulțime de dublări. Toate testele noastre trec, așa că haideți să refactor:

 funcția displayScore () dacă outOfRegularScore $ 2 $ 4; apoi checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 altceva echo "$ 1: 'convertToTennisScore $ 2' - $ 3: funcția 'convertToTennisScore $ 4' 'fi outOfRegularScore () [$ 1 -gt2] && [$ 2 -gt2]  funcția checkEquality () if [$ 1 -eq $ 2]; apoi echo funcția "Deuce" fi checkFirstPlayerAdv () dacă [$ 2 -gt $ 3]; apoi echo "$ 1: Advantage" fi

Acest lucru va funcționa pentru moment. Să testăm avantajul pentru al doilea jucător:

 funcția testItCanOutputAdvantageForSecondPlayer () assertEquals "Michael: Advantage" "'displayScore' John '3' Michael '4'"

Și codul:

 funcția displayScore () dacă outOfRegularScore $ 2 $ 4; apoi checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 3 $ 4 alt echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4' 'fi funcția checkAdvantage () if [$ 2 -gt $ 4]; apoi faceți clic pe "$ 1: Advantage" elif [$ 4 -gt $ 2]; apoi echo "$ 3: Advantage" fi

Acest lucru funcționează, dar avem o oarecare dublare în checkAdvantage funcţie. Să o simplificăm și să o numim de două ori:

 funcția displayScore () dacă outOfRegularScore $ 2 $ 4; apoi checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 4 checkAdvantage $ 3 $ 4 $ 2 altceva echo $ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4' 'fi funcția checkAdvantage () if [$ 2 -gt $ 3]; apoi echo "$ 1: Advantage" fi

Aceasta este de fapt mai bună decât soluția noastră anterioară și revine la implementarea inițială a acestei metode. Dar acum avem o altă problemă: mă simt inconfortabil cu $ de 1, $ cu 2, $ cu 3 și $ cu 4 variabile. Ei au nevoie de nume semnificative:

 funcția displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = $ 4 dacă outOfRegularScore $ firstPlayerScore $ secondPlayerScore; apoi checkEquality $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ firstPlayerName $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ secondPlayerName $ secondPlayerScore $ firstPlayerScore altundeva echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4' 'fi funcție checkAdvantageFor () if [$ 2 -gt $ 3 ]; apoi echo "$ 1: Advantage" fi

Acest lucru face codul nostru mai lung, dar este mult mai expresiv. imi place.

Este timpul să găsim un câștigător:

 function testItCanOutputWinnerForFirstPlayer () assertEquals 'John: Câștigător' "'displayScore' John '5' Michael '3'"

Trebuie doar să modificăm checkAdvantageFor funcţie:

 funcția checkAdvantageFor () dacă [$ 2 -gt $ 3]; atunci dacă ['expr $ 2 - $ 3' -gt 1]; apoi echo "$ 1: Winner" altceva echo "$ 1: Advantage" fi fi

Suntem aproape gata! În ultimul nostru pas, vom scrie codul tennisGame.sh pentru a trece testul de acceptare. Acesta va fi un cod destul de simplu:

 #! / usr / bin / env sh ### tenisGame.sh ### ... /functions.sh jucătoriLine = "cap -n 1 $ 1" ecou "$ playersLine" firstPlayer = "getFirstPlayerFrom" $ playersLine "" secondPlayer = "getSecondPlayerFrom" $ jucătoriLine "" fullScoreFileContent = "pisică $ 1" totalNoOfLines = "echo" $ entireScoreFileContent "| wc -l" pentru currentLine în 'seq 2 $ totalNoOfLines' face firstPlayerScore = $ (getScoreFor $ firstPlayer "'echo \" $ entireScoreFileContent \ "| -n $ currentLine ") secondPlayerScore = $ (getScoreFor $ secondPlayer" 'echo \ "$ fullScoreFileContent \" | cap -n $ currentLine ") displayScore $ firstPlayer $ firstPlayerScore $ secondPlayer $ secondPlayerScore terminat

Am citit prima linie pentru a prelua numele celor doi jucători și apoi am citit treptat fișierul pentru a calcula scorul.


Gândurile finale

Shell script-urile pot crește ușor de la câteva linii de cod la câteva sute de linii. Când se întâmplă acest lucru, întreținerea devine tot mai dificilă. Folosind TDD și unitatea de testare poate ajuta foarte mult pentru a face script-ul dvs. complexe mai ușor de întreținut - să nu mai vorbim că vă forțează pentru a construi script-uri complexe într-o manieră mai profesionist.

Cod