Scripts de shell de test

Écrire des scripts shell ressemble beaucoup à la programmation. Certains scripts nécessitent peu de temps; d’autres scripts complexes peuvent nécessiter réflexion, planification et un engagement plus important. De ce point de vue, il est judicieux d’adopter une approche axée sur les tests et de tester à la fois nos scripts shell..

Pour tirer le meilleur parti de ce didacticiel, vous devez vous familiariser avec l'interface de ligne de commande (CLI). vous pouvez consulter le didacticiel «La ligne de commande est votre meilleur ami» si vous avez besoin d'un rappel. Vous devez également avoir une compréhension de base des scripts shell de type Bash. Enfin, vous voudrez peut-être vous familiariser avec les concepts de développement piloté par les tests (TDD) et les tests unitaires en général; assurez-vous de consulter ces didacticiels PHP axés sur les tests pour avoir une idée de base.


Préparer l'environnement de programmation

Tout d'abord, vous avez besoin d'un éditeur de texte pour écrire vos scripts shell et vos tests unitaires. Utilisez votre favori!

Nous allons utiliser le framework de tests unitaires shUnit2 pour exécuter nos tests unitaires. Il a été conçu pour les coquilles Bash-like et fonctionne avec. shUnit2 est un framework open source publié sous licence GPL, et une copie du framework est également incluse avec le code source de ce tutoriel..

L'installation de shUnit2 est très simple. Il suffit de télécharger et d'extraire l'archive à n'importe quel emplacement de votre disque dur. Il est écrit en bash et, en tant que tel, le framework est composé uniquement de fichiers de script. Si vous prévoyez d'utiliser fréquemment shUnit2, je vous recommande fortement de le placer à un emplacement de votre chemin..


Écrire notre premier test

Pour ce tutoriel, extrayez shUnit dans un répertoire portant le même nom dans votre Sources dossier (voir le code joint à ce tutoriel). Créer un Des tests dossier à l'intérieur Sources et ajouté un nouvel appel de fichier firstTest.sh.

 #! / usr / bin / env sh ### firstTest.sh ### function testWeCanWriteTests () assertEquals "ça marche" "ça marche" ## Appelez et exécutez tous les tests. "… /Shunit2-2.1.6/src/shunit2"

Que rendre votre fichier de test exécutable.

$ cd __votre_code_folder __ / Teste $ chmod + x firstTest.sh

Maintenant, vous pouvez simplement l'exécuter et observer le résultat:

 $ ./firstTest.sh testWeCanWriteTests Ran 1 test. D'accord

Il dit que nous avons effectué un test réussi. Faisons en sorte que le test échoue. changer la assertEquals afin que les deux chaînes ne soient pas identiques et relancez le test:

 $ ./firstTest.sh testWeCanWriteTests ASSERT: attendu: mais était: Ran 1 test. FAILED (échecs = 1)

Un jeu de tennis

Vous écrivez des tests d'acceptation au début d'un projet / article / récit lorsque vous pouvez définir clairement une exigence spécifique..

Maintenant que nous avons un environnement de test fonctionnel, écrivons un script qui lit un fichier, prend des décisions en fonction du contenu du fichier et affiche les informations à l'écran..

L'objectif principal du script est de montrer le score d'un match de tennis entre deux joueurs. Nous nous concentrerons uniquement sur le maintien du score d'un seul match; Tout le reste dépend de toi. Les règles de notation sont les suivantes:

  • Au début, chaque joueur a un score de zéro, appelé "amour"
  • Les premier, deuxième et troisième balles gagnées sont marqués "quinze", "trente" et "quarante".
  • Si à "quarante" le score est égal, on l'appelle "deux".
  • Après cela, le score est conservé comme "avantage" pour le joueur qui marque un point de plus que l'autre joueur..
  • Un joueur est le gagnant s'il parvient à avoir un avantage d'au moins deux points et gagne au moins trois points (c'est-à-dire s'il atteint au moins "quarante").

Définition de l'entrée et de la sortie

Notre application va lire le score à partir d'un fichier. Un autre système va pousser les informations dans ce fichier. La première ligne de ce fichier de données contiendra les noms des joueurs. Lorsqu'un joueur marque un point, son nom est écrit à la fin du fichier. Un fichier de partition typique ressemble à ceci:

 John - Michael John John Michael John Michael Michael John John

Vous pouvez trouver ce contenu dans le input.txt déposer dans le La source dossier.

La sortie de notre programme écrit la partition à l’écran une ligne à la fois. Le résultat devrait être:

 John - Michael John: 15 - Michael: 0 John: 30 - Michael: 0 John: 30 - Michael: 15 John: 40 - Michael: 15 John: 40 - Michael: 30 ans Deuce John: Avantage John: Gagnant

Cette sortie peut également être trouvée dans le output.txt fichier. Nous utiliserons ces informations pour vérifier si notre programme est correct.


Le test d'acceptation

Vous écrivez des tests d'acceptation au début d'un projet / long métrage / récit lorsque vous pouvez définir clairement une exigence spécifique. Dans notre cas, ce test appelle simplement notre script bientôt créé avec le nom du fichier d'entrée en tant que paramètre et attend que la sortie soit identique au fichier manuscrit de la section précédente:

 #! / usr / bin / env sh ### receiveTest.sh ### function testItCanProvideAllTheScores () cd… /tennisGame.sh ./input.txt> ./results.txt diff. /output.txt ./results.txt 'La production attendue diffère.' $?  ## Appelez et lancez tous les tests. "… /Shunit2-2.1.6/src/shunit2"

Nous allons exécuter nos tests dans le Source / Tests dossier; donc, CD… nous emmène dans le La source annuaire. Ensuite, il essaie de courir tennisGamse.sh, qui n'existe pas encore. Puis le diff La commande comparera les deux fichiers: ./output.txt est notre sortie manuscrite et ./results.txt contiendra le résultat de notre script. finalement, affirmer vrai vérifie la valeur de sortie de diff.

Mais pour l'instant, notre test renvoie l'erreur suivante:

 $ ./acceptanceTest.sh testItCanProvideAllTheScores ./acceptanceTest.sh: ligne 7: tennisGame.sh: commande non trouvée diff: ./results.txt: Aucun fichier ou répertoire de ce type ASSERT: La sortie attendue est différente. Ran 1 test. FAILED (échecs = 1)

Faisons de ces erreurs un échec intéressant en créant un fichier vide appelé tennisGame.sh et le rendre exécutable. Maintenant, lorsque nous exécutons notre test, nous n'obtenons pas d'erreur:

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

Mise en œuvre avec TDD

Créez un autre fichier appelé unitTests.sh pour nos tests unitaires. Nous ne voulons pas exécuter notre script pour chaque test; nous voulons seulement exécuter les fonctions que nous testons. Donc, nous ferons tennisGame.sh exécuter uniquement les fonctions qui résideront dans functions.sh:

 #! / usr / bin / env sh ### unitTest.sh ### source… /functions.sh fonction testItCanProvideFirstPlayersName () assertEquals 'John "getFirstPlayerFrom' John - Michael" ## Appelez et exécutez tous les tests. "… /Shunit2-2.1.6/src/shunit2"

Notre premier test est simple. Nous essayons de récupérer le nom du premier joueur lorsqu'une ligne contient deux noms séparés par un trait d'union. Ce test échouera car nous n’avons pas encore de getFirstPlayerFrom une fonction:

 $ ./unitTest.sh testItCanProvideFirstPlayersName ./unitTest.sh: ligne 8: getFirstPlayerFrom: commande introuvable shunit2: ERREUR assertEquals () requiert deux ou trois arguments; 1 donnée shunit2: ERREUR 1: Jean 2: 3: Ran 1 test. D'accord

La mise en œuvre pour getFirstPlayerFromest très simple. C'est une expression régulière qui est poussée à travers le sed commander:

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

Maintenant, le test réussit:

 $ ./unitTest.sh testItCanProvideFirstPlayersNameName Ran 1 test. D'accord

Écrivons un autre test pour le nom du second joueur:

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

L'échec:

 ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName ASSERT: attendu: mais était: A couru 2 tests. FAILED (échecs = 1)

Et maintenant, l'implémentation de la fonction pour la faire passer:

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

Maintenant nous avons des tests de passage:

$ ./unitTest.sh testItCanProvideFirstPlayersName testItCanProvideSecondPlayersName Ran 2 tests. D'accord

Accélérons les choses

À partir de ce moment, nous écrirons un test et la mise en œuvre, et je n’expliquerai que ce qui mérite d’être mentionné..

Testons si nous avons un joueur avec un seul score. Ajout du test suivant:

 fonction testItCanGetScoreForAPlayerWithOnlyOneWin () classements = $ 'John - Michael \ nJohn' assertEquals '1 "getScoreFor' John '" $ classements "'

Et la solution:

 fonction getScoreFor () player = $ 1 classement = $ 2 totalMatches = $ (echo "$ classement" | grep $ player | wc -l) echo $ (($ totalMatches-1))

Nous utilisons des citations fantaisistes pour passer la séquence newline (\ n) dans un paramètre de chaîne. Ensuite on utilise grep pour trouver les lignes qui contiennent le nom du joueur et les compter avec toilettes. Enfin, on soustrait un du résultat pour neutraliser la présence de la première ligne (elle ne contient que des données non liées au score).

Nous sommes maintenant à la phase de refactoring de TDD.

Je viens de me rendre compte que le code fonctionne réellement pour plus d'un point par joueur, et nous pouvons refactoriser nos tests pour refléter cela. Modifiez la fonction de test ci-dessus comme suit:

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

Les tests passent toujours. Il est temps d'aller de l'avant avec notre logique:

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

Et la mise en place:

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

Je ne vérifie que le deuxième paramètre. On dirait que je triche, mais c'est le code le plus simple pour réussir le test. Écrire un autre test nous oblige à ajouter plus de logique, mais quel test devrions-nous écrire ensuite?

Nous pouvons emprunter deux chemins. Tester si le deuxième joueur reçoit un point nous oblige à en écrire un autre si déclaration, mais nous n'avons qu'à ajouter un autre déclaration si nous choisissons de tester le deuxième point du premier joueur. Ce dernier implique une implémentation plus facile, essayons donc ceci:

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

Et la mise en place:

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

Cela ressemble encore à de la triche, mais cela fonctionne parfaitement. Continuant sur le troisième point:

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

La mise en oeuvre:

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

Ce si-elif-else commence à m'agacer. Je veux le changer, mais revoyons d’abord nos tests. Nous avons trois tests très similaires; alors écrivons-les dans un seul test qui fait trois affirmations:

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

C'est mieux, et ça passe encore. Créons maintenant un test similaire pour le deuxième joueur:

 fonction 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'exécution de ce test donne des résultats intéressants:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: attendu: mais était: ASSERT: attendu: mais était: ASSERT: attendu: mais était:

C'était inattendu. Nous savions que Michael aurait des scores incorrects. La surprise est John; il devrait avoir 0 pas 40. Réparons cela en modifiant d'abord le si-elif-else expression:

 fonction 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 écho "$ 1: $ playerOneScore - $ 3: $ 4"

le si-elif-else est maintenant plus complexe, mais nous avons au moins corrigé les scores de John:

 testItCanOutputScoreWhenSecondPlayerWinsFirst3Points ASSERT: attendu: mais était: ASSERT: attendu: mais était: ASSERT: attendu: mais était:

Maintenant, réparons 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 écho $ playerOneScore; 

Cela a bien fonctionné! Maintenant, il est temps de refacturer enfin ce vilain si-elif-else expression:

 fonction convertToTennisScore () déclarer -a scoreMap = ('0 "15" 30 "40') echo $ scoreMap [$ 1];

Les cartes de valeur sont merveilleux! Passons au cas "Deuce":

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

Nous vérifions "Deuce" lorsque tous les joueurs ont au moins un score de 40.

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

Maintenant, nous testons l'avantage du premier joueur:

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

Et pour le faire passer:

 fonction displayScore () if [$ 2 -gt 2] && [$ 4 -gt 2] && [$ 2 -eq $ 4]; then 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

Il y a que moche si-elif-else encore une fois, et nous avons beaucoup de duplication aussi. Tous nos tests sont réussis, alors refactorisons:

 fonction displayScore () if outOfRegularScore $ 2 $ 4; then checkEquality $ 2 $ 4 checkFirstPlayerAdv $ 1 $ 2 $ 4 else echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore $ 4'" fi fonction outOfRegularScore () [$ 1 -gt 2] && [$ 2 -gt 2] renvoyer $?  function checkEquality () if [$ 1 -eq $ 2]; then echo "Deuce" fi function checkFirstPlayerAdv () if [$ 2 -gt $ 3]; puis echo "$ 1: Advantage" fi

Ça va marcher pour le moment. Testons l'avantage pour le deuxième joueur:

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

Et le code:

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

Cela fonctionne, mais nous avons quelques doublons dans le checkAdvantage une fonction. Simplifions le et appelons-le deux fois:

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

C’est en fait meilleur que notre solution précédente et cela revient à l’implémentation originale de cette méthode. Mais nous avons maintenant un autre problème: je me sens mal à l’aise 1 $, 2 $, 3 $ et 4 $ variables. Ils ont besoin de noms significatifs:

 fonction displayScore () firstPlayerName = $ 1; firstPlayerScore = $ 2 secondPlayerName = $ 3; secondPlayerScore = 4 $ si outOfRegularScore $ firstPlayerScore $ secondPlayerScore; then checkEquality $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ firstPlayerName $ firstPlayerScore $ secondPlayerScore checkAdvantageFor $ secondPlayerScore $ firstPlayerScore sinon echo "$ 1: 'convertToTennisScore $ 2' - $ 3: 'convertToTennisScore' 4 '' fi" ]; puis echo "$ 1: Advantage" fi

Cela rallonge notre code, mais il est nettement plus expressif. je l'aime.

Il est temps de trouver un gagnant:

 fonction testItCanOutputWinnerForFirstPlayer () assertEquals 'John: Gagnant' "'displayScore' John '5' Michael '3'"

Il suffit de modifier le checkAdvantageFor une fonction:

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

On a presque fini! Comme dernière étape, nous écrirons le code tennisGame.sh faire passer le test d'acceptation. Ce sera un code assez simple:

 #! / usr / bin / env sh ### tennisGame.sh ###… /functions.sh playersLine = "tête -n 1 $ 1" echo "$ playersLine" firstPlayer = "getFirstPlayerDe" $ playersLine "" secondPlayer = "getSecondPlayerFrom" $ playersLine "" wholeScoreFileContent = "cat $ 1" totalNoOfLines = "echo" $ wholeScoreFileContent "| wc -l" pour currentLine dans 'seq 2 $ totalNoOfLines' do firstPlayerScore = $ (getScoreFor $ firstPlayer "echo \" $ wholeScoreFileContent \ "| -n $ currentLine '") secondPlayerScore = $ (getScoreFor $ secondPlayer"' echo \ "$ wholeScoreFileContent \" | head -n $ currentLine '") displayScore $ firstPlayer $ firstPlayerScore $ secondPlayer $ secondPlayerScore done

Nous lisons la première ligne pour récupérer les noms des deux joueurs, puis nous lisons progressivement le fichier pour calculer le score..


Dernières pensées

Les scripts shell peuvent facilement passer de quelques lignes de code à quelques centaines de lignes. Lorsque cela se produit, la maintenance devient de plus en plus difficile. L'utilisation de TDD et des tests unitaires peut grandement faciliter la maintenance de vos scripts complexes. Il est également important de vous obliger à créer vos scripts complexes de manière plus professionnelle..