Script test-guida di shell

Scrivere script di shell è molto simile alla programmazione. Alcuni script richiedono un investimento di poco tempo; mentre altri script complessi possono richiedere pensiero, pianificazione e un impegno più ampio. Da questo punto di vista, ha senso adottare un approccio basato su test e un test unitario sugli script della shell.

Per ottenere il massimo da questo tutorial, è necessario avere familiarità con l'interfaccia a riga di comando (CLI); se vuoi un aggiornamento, ti consigliamo di dare un'occhiata al tutorial di The Command Line è il tuo migliore amico. Hai anche bisogno di una conoscenza di base dello scripting di shell di tipo Bash. Infine, potresti voler familiarizzare con i concetti dello sviluppo basato sui test (TDD) e dei test unitari in generale; assicurati di dare un'occhiata a questi tutorial PHP testati per ottenere l'idea di base.


Preparare l'ambiente di programmazione

Innanzitutto, è necessario un editor di testo per scrivere script di shell e test di unità. Usa il tuo preferito!

Useremo il framework di test dell'unità shell shUnit2 per eseguire i nostri test unitari. È stato progettato per, e funziona con, shell di tipo Bash. shUnit2 è un framework open source rilasciato sotto licenza GPL e una copia del framework è inclusa anche con il codice sorgente di questo tutorial.

Installare shUnit2 è molto semplice; semplicemente scarica ed estrai l'archivio in qualsiasi posizione sul tuo disco rigido. È scritto in Bash e, come tale, il framework consiste solo di file di script. Se prevedi di utilizzare frequentemente shUnit2, ti consiglio vivamente di metterlo in una posizione nel PERCORSO.


Scrivi il nostro primo test

Per questo tutorial, estrai shUnit in una directory con lo stesso nome nel tuo fonti cartella (vedi il codice allegato a questo tutorial). Creare un test cartella interna fonti e ha aggiunto una nuova chiamata al file firstTest.sh.

 #! / usr / bin / env sh ### firstTest.sh ### function testWeCanWriteTests () assertEquals "funziona" "funziona" ## Chiama ed esegui tutti i test. "... /shunit2-2.1.6/src/shunit2"

Quindi rendere il file eseguibile del test.

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

Ora puoi semplicemente eseguirlo e osservare l'output:

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

Dice che abbiamo condotto un test di successo. Ora, facciamo fallire il test; cambiare il assertEquals dichiarazione in modo che le due stringhe non siano le stesse ed eseguire nuovamente il test:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: previsto: ma era: Ran 1 test. NON RIUSCITO (guasti = 1)

Un gioco di tennis

Scrivi test di accettazione all'inizio di un progetto / funzione / storia quando puoi definire chiaramente un requisito specifico.

Ora che abbiamo un ambiente di test funzionante, scriviamo uno script che legge un file, prende decisioni in base al contenuto del file e trasmette le informazioni allo schermo.

L'obiettivo principale della sceneggiatura è mostrare il punteggio di una partita di tennis tra due giocatori. Ci concentreremo solo sul mantenimento del punteggio di una singola partita; tutto il resto dipende da te. Le regole di punteggio sono:

  • All'inizio, ogni giocatore ha un punteggio pari a zero, chiamato "amore"
  • Il primo, il secondo e il terzo pallone vinti sono contrassegnati come "quindici", "trenta" e "quaranta".
  • Se a "quaranta" il punteggio è uguale, si chiama "deuce".
  • Dopo questo, il punteggio viene mantenuto come "vantaggio" per il giocatore che ottiene un punto in più rispetto all'altro giocatore.
  • Un giocatore è il vincitore se riesce ad avere un vantaggio di almeno due punti e vince almeno tre punti (cioè, se ha raggiunto almeno "quaranta").

Definizione di input e output

La nostra applicazione leggerà il punteggio da un file. Un altro sistema spingerà le informazioni in questo file. La prima riga di questo file di dati conterrà i nomi dei giocatori. Quando un giocatore segna un punto, il suo nome viene scritto alla fine del file. Un tipico file di punteggio assomiglia a questo:

 John - Michael John John Michael John Michael Michael John John

Puoi trovare questo contenuto nel input.txt file nel fonte cartella.

L'output del nostro programma scrive il punteggio sullo schermo una riga alla volta. L'output dovrebbe essere:

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

Questa uscita può anche essere trovata nel output.txt file. Useremo queste informazioni per verificare se il nostro programma è corretto.


Il test di accettazione

Scrivi test di accettazione all'inizio di un progetto / funzione / storia quando puoi definire chiaramente un requisito specifico. Nel nostro caso, questo test chiama semplicemente il nostro script appena creato con il nome del file di input come parametro, e si aspetta che l'output sia identico al file scritto a mano della sezione precedente:

 #! / usr / bin / env sh ### acceptanceTest.sh ### function testItCanProvideAllTheScores () cd ... /tennisGame.sh ./input.txt> ./results.txt diff ./output.txt ./results.txt assertTrue 'L'uscita prevista è diversa.' $?  ## Chiama ed esegui tutti i test. "... /shunit2-2.1.6/src/shunit2"

Eseguiremo i nostri test nel Fonte / Test cartella; perciò, CD… ci porta nel fonte directory. Quindi prova a correre tennisGamse.sh, che non esiste ancora. Poi il diff comando confronterà i due file: ./output.txt è la nostra produzione scritta a mano e ./results.txt conterrà il risultato del nostro script. Finalmente, assertTrue controlla il valore di uscita di diff.

Ma per ora, il nostro test restituisce il seguente errore:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores ./acceptanceTest.sh: line 7: tennisGame.sh: comando non trovato diff: ./results.txt: nessun file o directory ASSERT: l'output previsto differisce. Ran 1 test. NON RIUSCITO (guasti = 1)

Trasformiamo quegli errori in un bel fallimento creando un file vuoto chiamato tennisGame.sh e renderlo eseguibile. Ora, quando eseguiamo il nostro test, non riceviamo un errore:

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

Implementazione con TDD

Crea un altro file chiamato unitTests.sh per i nostri test unitari. Non vogliamo eseguire il nostro script per ogni test; vogliamo solo eseguire le funzioni che testiamo. Quindi, faremo tennisGame.sh esegui solo le funzioni in cui risiederà functions.sh:

 #! / usr / bin / env sh ### unitTest.sh ### source ... /functions.sh function testItCanProvideFirstPlayersName () assertEquals 'John "getFirstPlayerFrom' John - Michael" ## Chiama ed esegui tutti i test. "... /shunit2-2.1.6/src/shunit2"

Il nostro primo test è semplice. Cerchiamo di recuperare il nome del primo giocatore quando una riga contiene due nomi separati da un trattino. Questo test fallirà perché non abbiamo ancora un getFirstPlayerFrom funzione:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: riga 8: getFirstPlayerFrom: comando non trovato shunit2: ERROR assertEquals () richiede due o tre argomenti; 1 data shunit2: ERRORE 1: John 2: 3: test Ran 1. ok

L'implementazione per getFirstPlayerFromè molto semplice È un'espressione regolare che viene spinta attraverso il sed comando:

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

Ora il test passa:

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

Scriviamo un altro test per il nome del secondo giocatore:

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

Il fallimento:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: previsto: ma era: Esegui 2 test. NON RIUSCITO (guasti = 1)

E ora l'implementazione della funzione per farlo passare:

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

Ora abbiamo superato i test:

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

Facciamo accelerare le cose

A partire da questo punto, scriveremo un test e l'implementazione, e spiegherò solo ciò che merita di essere menzionato.

Proviamo se abbiamo un giocatore con un solo punteggio. Aggiunto il seguente test:

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

E la soluzione:

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

Usiamo qualche citazione di fantasia per passare la sequenza di nuova riga (\ n) all'interno di un parametro stringa. Quindi usiamo grep per trovare le linee che contengono il nome del giocatore e contarle bagno. Infine, sottraiamo uno dal risultato per neutralizzare la presenza della prima riga (contiene solo dati non relativi ai punteggi).

Ora siamo nella fase di refactoring di TDD.

Mi sono appena reso conto che il codice in realtà funziona per più di un punto per giocatore, e possiamo refactoring i nostri test per riflettere questo. Modificare la suddetta funzione di test al seguente:

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

I test continuano ancora. È ora di andare avanti con la nostra logica:

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

E l'implementazione:

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

Controllo solo il secondo parametro. Sembra che io stia imbrogliando, ma è il codice più semplice per far passare il test. Scrivere un altro test ci obbliga ad aggiungere più logica, ma quale test dovremmo scrivere dopo?

Ci sono due percorsi che possiamo prendere. Testare se il secondo giocatore riceve un punto ci costringe a scriverne un altro Se dichiarazione, ma dobbiamo solo aggiungere un altro affermazione se scegliamo di testare il secondo punto del primo giocatore. Quest'ultimo implica un'implementazione più semplice, quindi proviamo:

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

E l'implementazione:

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

Questo sembra ancora imbrogliare, ma funziona perfettamente. Proseguendo per il terzo punto:

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

L'implemento:

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"

Questo if-elif-else sta iniziando a infastidirmi. Voglio cambiarlo, ma prima rifattiamo i nostri test. Abbiamo tre test molto simili; quindi scriviamoli in un unico test che faccia tre asserzioni:

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

È meglio, e passa ancora. Ora, creiamo un test simile per il secondo giocatore:

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

L'esecuzione di questo test produce risultati interessanti:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: previsto: ma era: ASSERT: atteso: ma era: ASSERT: atteso: ma era:

Beh, quello era inaspettato. Sapevamo che Michael avrebbe avuto punteggi errati. La sorpresa è John; dovrebbe avere 0 non 40. Risolviamolo modificando prima il if-elif-else espressione:

 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"

Il if-elif-else è ora più complesso, ma abbiamo almeno corretto i punteggi di John:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: previsto: ma era: ASSERT: atteso: ma era: ASSERT: atteso: ma era:

Ora sistemiamo Michael:

 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; 

Ha funzionato bene! Ora è il momento di ridefinire finalmente quello brutto if-elif-else espressione:

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

Le mappe del valore sono meravigliose! Passiamo al caso "Deuce":

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

Controlliamo "Deuce" quando tutti i giocatori hanno almeno un punteggio di 40.

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

Ora proviamo per il vantaggio del primo giocatore:

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

E per farlo passare:

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

C'è quel brutto if-elif-else di nuovo, e abbiamo anche molte duplicazioni. Passano tutti i nostri test, quindi facciamo il refactoring:

 function displayScore () if outOfRegularScore $ 2 $ 4; quindi checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 altro echo "$ 1: 'convertToTennisScore $ 2' - $ 3: '$ ConvertToTennisScore'" funzione fi outOfRegularScore () [$ 1 -gt 2] && [$ 2 -gt 2] ritorno $?  function checkEquality () if [$ 1 -eq $ 2]; quindi echo "Deuce" fi function checkFirstPlayerAdv () if [$ 2 -gt $ 3]; quindi echo "$ 1: vantaggio" fi

Funzionerà per ora. Proviamo il vantaggio per il secondo giocatore:

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

E il codice:

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

Funziona, ma abbiamo alcune duplicazioni nel checkAdvantage funzione. Semplifichiamolo e chiamiamolo due volte:

 function displayScore () if outOfRegularScore $ 2 $ 4; quindi checkEquality $ 2 $ 4 checkAdvantage $ 1 $ 2 $ 4 checkAdvantage $ 3 $ 4 $ 2 altro echo "$ 1: 'convertToTennisScore $ 2' - $ 3: '$ ConvertToTennisScore'" fi function checkAdvantage () if [$ 2 -gt $ 3]; quindi echo "$ 1: vantaggio" fi

Questo è in realtà migliore della nostra soluzione precedente e ritorna all'implementazione originale di questo metodo. Ma ora abbiamo un altro problema: mi sento a disagio con il $ 1, $ 2, $ 3 e $ 4 variabili. Hanno bisogno di nomi significativi:

 function displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = $ 4 se outOfRegularScore $ firstPlayerScore $ secondPlayerScore; poi 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 ]; quindi echo "$ 1: vantaggio" fi

Questo rende il nostro codice più lungo, ma è significativamente più espressivo. mi piace.

È tempo di trovare un vincitore:

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

Dobbiamo solo modificare il checkAdvantageFor funzione:

 function checkAdvantageFor () if [$ 2 -gt $ 3]; quindi se ['expr $ 2 - $ 3' -gt 1]; then echo "$ 1: Winner" else echo "$ 1: Advantage" fi fi

Abbiamo quasi finito! Come ultimo passo, scriveremo il codice tennisGame.sh per passare il test di accettazione. Questo sarà un codice abbastanza semplice:

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

Leggiamo la prima riga per recuperare i nomi dei due giocatori, quindi leggiamo in modo incrementale il file per calcolare il punteggio.


Pensieri finali

Gli script di shell possono facilmente passare da poche righe di codice a poche centinaia di righe. Quando ciò accade, la manutenzione diventa sempre più difficile. L'utilizzo del TDD e dei test delle unità può aiutare a rendere più facile la manutenzione del tuo script complesso, senza contare che ti costringe a creare script complessi in modo più professionale.