Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 26, 2013 09:52 pm GMT

Test-Driving Shell Scripts

Writing shell scripts is very much like programming. Some scripts require little time investment; whereas, other complex scripts may require thought, planning and a larger commitment. From this perspective, it makes sense to take a test-driven approach and unit test our shell scripts.

To get the most out of this tutorial, you need to be familiar with the command line interface (CLI); you may want to check out the The Command Line is Your Best Friend tutorial if you need a refresher. You also need a basic understanding of Bash-like shell scripting. Finally, you may want to familiarize yourself with the test-driven development (TDD) concepts and unit testing in general; be sure to check out these Test-Driven PHP tutorials to get the basic idea.


Prepare the Programming Environment

First, you need a text editor to write your shell scripts and unit tests. Use your favorite!

We will use the shUnit2 shell unit testing framework to run our unit tests. It was designed for, and works with, Bash-like shells. shUnit2 is an open source framework released under the GPL license, and a copy of the framework is also included with this tutorial’s sample source code.

Installing shUnit2 is very easy; simply download and extract the archive to any location on your hard drive. It is written in Bash, and as such, the framework consists of only script files. If you plan to frequently use shUnit2, I highly recommend that you put it in a location in your PATH.


Writing our First Test

For this tutorial, extract shUnit into a directory with the same name in your Sources folder (see the code attached to this tutorial). Create a Tests folder inside Sources and added a new file call firstTest.sh.

#! /usr/bin/env sh### firstTest.sh ###function testWeCanWriteTests () {assertEquals "it works" "it works"}## Call and Run all Tests. "../shunit2-2.1.6/src/shunit2"

Than make your test file executable.

$ cd __your_code_folder__/Tests$ chmod +x firstTest.sh

Now you can simply run it and observe the output:

$ ./firstTest.shtestWeCanWriteTestsRan 1 test.OK

It says we ran one successful test. Now, let’s cause the test to fail; change the assertEquals statement so that the two strings are not the same and run the test again:

$ ./firstTest.shtestWeCanWriteTestsASSERT:expected:<it works> but was:<it does not work>Ran 1 test.FAILED (failures=1)

A Tennis Game

You write acceptance tests at the beginning of a project/feature/story when you can clearly define a specific requirement.

Now that we have a working testing environment, let’s write a script that reads a file, makes decisions based on the file’s contents and outputs information to the screen.

The main goal of the script is to show the score of a tennis game between two players. We will concentrate only on keeping the score of a single game; everything else is up to you. The scoring rules are:

  • At the beginning, each player has a score of zero, called “love”
  • First, second and third balls won are marked as “fifteen”, “thirty”, and “forty”.
  • If at “forty” the score is equal, it is called “deuce”.
  • After this, the score is kept as “Advantage” for the player who scores one more point than the other player.
  • A player is the winner if he manages to have an advantage of at least two points and wins at least three points (that is, if he reached at least “forty”).

Definition of Input and Output

Our application will read the score from a file. Another system will push the information into this file. The first line of this data file will contain the names of the players. When a player scores a point, their name is written at the end of the file. A typical score file looks like this:

John - MichaelJohnJohnMichaelJohnMichaelMichaelJohnJohn

You can find this content in the input.txt file in the Source folder.

The output of our program writes the score to the screen one line at a time. The output should be:

John - MichaelJohn: 15 - Michael: 0John: 30 - Michael: 0John: 30 - Michael: 15John: 40 - Michael: 15John: 40 - Michael: 30DeuceJohn: AdvantageJohn: Winner

This output can be also found in the output.txt file. We will use this information to check if our program is correct.


The Acceptance Test

You write acceptance tests at the beginning of a project/feature/story when you can clearly define a specific requirement. In our case, this test simply calls our soon-to-be-created script with the name of the input file as the parameter, and it expects the output to be identical with the hand-written file from the previous section:

#! /usr/bin/env sh### acceptanceTest.sh ###function testItCanProvideAllTheScores () {cd .../tennisGame.sh ./input.txt > ./results.txtdiff ./output.txt ./results.txtassertTrue 'Expected output differs.' $?}## Call and Run all Tests. "../shunit2-2.1.6/src/shunit2"

We will run our tests in the Source/Tests folder; therefore, cd .. takes us into the Source directory. Then it tries to run tennisGamse.sh, which does not yet exist. Then the diff command will compare the two files: ./output.txt is our hand-written output and ./results.txt will contain the result of our script. Finally, assertTrue checks the exit value of diff.

But for now, our test returns the following error:

$ ./acceptanceTest.shtestItCanProvideAllTheScores./acceptanceTest.sh: line 7: tennisGame.sh: command not founddiff: ./results.txt: No such file or directoryASSERT:Expected output differs.Ran 1 test.FAILED (failures=1)

Let’s turn those errors into a nice failure by creating an empty file called tennisGame.sh and make it executable. Now when we run our test, we don’t get an error:

./acceptanceTest.shtestItCanProvideAllTheScores1,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: WinnerASSERT:Expected output differs.Ran 1 test.FAILED (failures=1)

Implementation with TDD

Create another file called unitTests.sh for our unit tests. We don’t want to run our script for each test; we only want to run the functions that we test. So, we will make tennisGame.sh run only the functions that will reside in functions.sh:

#! /usr/bin/env sh### unitTest.sh ###source ../functions.shfunction testItCanProvideFirstPlayersName () {assertEquals 'John' `getFirstPlayerFrom 'John - Michael'`}## Call and Run all Tests. "../shunit2-2.1.6/src/shunit2"

Our first test is simple. We attempt to retrieve the first player’s name when a line contains two names separated by a hyphen. This test will fail because we do not yet have a getFirstPlayerFrom function:

$ ./unitTest.shtestItCanProvideFirstPlayersName./unitTest.sh: line 8: getFirstPlayerFrom: command not foundshunit2:ERROR assertEquals() requires two or three arguments; 1 givenshunit2:ERROR 1: John 2:  3:Ran 1 test.OK

The implementation for getFirstPlayerFromis very simple. It’s a regular expression that is pushed through the sed command:

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

Now the test passes:

$ ./unitTest.shtestItCanProvideFirstPlayersNameRan 1 test.OK

Let’s write another test for the second player’s name:

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

The failure:

./unitTest.shtestItCanProvideFirstPlayersNametestItCanProvideSecondPlayersNameASSERT:expected:<Michael> but was:<John>Ran 2 tests.FAILED (failures=1)

And now the function implementation to make it pass:

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

Now we have passing tests:

$ ./unitTest.shtestItCanProvideFirstPlayersNametestItCanProvideSecondPlayersNameRan 2 tests.OK

Let’s Speed Things Up

Starting at this point, we will write a test and the implementation, and I will explain only what deserves to be mentioned.

Let’s test if we have a player with only one score. Added the following test:

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

And the solution:

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

We use some fancy-pants quoting to pass the newline sequence (\n) inside a string parameter. Then we use grep to find the lines that contain the player’s name and count them with wc. Finally, we subtract one from the result to counteract the presence of the first line (it contains only non-score related data).

Now we are at the refactoring phase of TDD.

I just realized that the code actually works for more than one point per player, and we can refactor our tests to reflect this. Change the above test function to the following:

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

The tests still passes. Time to move on with our logic:

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

And the implementation:

function displayScore () {if [ "$2" -eq '1' ]; thenplayerOneScore='15'fiecho "$1: $playerOneScore - $3: $4"}

I only check the second parameter. This looks like I’m cheating, but it is the simplest code to make the test pass. Writing another test forces us to add more logic, but what test should we write next?

There are two paths we can take. Testing if the second player recieves a point forces us to write another if statement, but we only have to add an else statement if we choose to test the first player’s second point. The latter implies an easier implementation, so let’s try that:

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

And the implementation:

function displayScore () {if [ "$2" -eq '1' ]; thenplayerOneScore='15'elseplayerOneScore='30'fiecho "$1: $playerOneScore - $3: $4"}

This still looks cheating, but it works perfectly. Continuing on for the third point:

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

The implementation:

function displayScore () {if [ "$2" -eq '1' ]; thenplayerOneScore='15'elif [ "$2" -eq '2' ]; thenplayerOneScore='30'elseplayerOneScore='40'fiecho "$1: $playerOneScore - $3: $4"}

This if-elif-else is starting to annoy me. I want to change it, but let’s first refactor our tests. We have three very similar tests; so let’s write them into a single test that makes three assertions:

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`"}

That’s better, and it still passes. Now, let’s create a similar test for the second player:

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`"}

Running this test results in interesting output:

testItCanOutputScoreWhenSecondPlayerWinsFirst3PointsASSERT:expected:<John: 0 - Michael: 15> but was:<John: 40 - Michael: 1>ASSERT:expected:<John: 0 - Michael: 30> but was:<John: 40 - Michael: 2>ASSERT:expected:<John: 0 - Michael: 40> but was:<John: 40 - Michael: 3>

Well that was unexpected. We knew that Michael would have incorrect scores. The surprise is John; he should have 0 not 40. Let’s fix that by first modifying the if-elif-else expression:

function displayScore () {if [ "$2" -eq '1' ]; thenplayerOneScore='15'elif [ "$2" -eq '2' ]; thenplayerOneScore='30'elif [ "$2" -eq '3' ]; thenplayerOneScore='40'elseplayerOneScore=$2fiecho "$1: $playerOneScore - $3: $4"}

The if-elif-else is now more complex, but we at least fixed the John’s scores:

testItCanOutputScoreWhenSecondPlayerWinsFirst3PointsASSERT:expected:<John: 0 - Michael: 15> but was:<John: 0 - Michael: 1>ASSERT:expected:<John: 0 - Michael: 30> but was:<John: 0 - Michael: 2>ASSERT:expected:<John: 0 - Michael: 40> but was:<John: 0 - Michael: 3>

Now let’s fix Michael:

function displayScore () {echo "$1: `convertToTennisScore $2` - $3: `convertToTennisScore $4`"}function convertToTennisScore () {if [ "$1" -eq '1' ]; thenplayerOneScore='15'elif [ "$1" -eq '2' ]; thenplayerOneScore='30'elif [ "$1" -eq '3' ]; thenplayerOneScore='40'elseplayerOneScore=$1fiecho $playerOneScore;}

That worked well! Now it’s time to finally refactor that ugly if-elif-else expression:

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

Value maps are wonderful! Let’s move on to the “Deuce” case:

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

We check for “Deuce” when all players have at least a score of 40.

function displayScore () {if [ $2 -gt 2 ] && [ $4 -gt 2 ] && [ $2 -eq $4 ]; thenecho "Deuce"elseecho "$1: `convertToTennisScore $2` - $3: `convertToTennisScore $4`"fi}

Now we test for the first player’s advantage:

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

And to make it pass:

function displayScore () {if [ $2 -gt 2 ] && [ $4 -gt 2 ] && [ $2 -eq $4 ]; thenecho "Deuce"elif [ $2 -gt 2 ] && [ $4 -gt 2 ] && [ $2 -gt $4 ]; thenecho "$1: Advantage"elseecho "$1: `convertToTennisScore $2` - $3: `convertToTennisScore $4`"fi}

There’s that ugly if-elif-else again, and we have a lot of duplication as well. All our tests pass, so let’s refactor:

function displayScore () {if outOfRegularScore $2 $4 ; thencheckEquality $2 $4checkFirstPlayerAdv $1 $2 $4elseecho "$1: `convertToTennisScore $2` - $3: `convertToTennisScore $4`"fi}function outOfRegularScore () {[ $1 -gt 2 ] && [ $2 -gt 2 ]return $?}function checkEquality () {if [ $1 -eq $2 ]; thenecho "Deuce"fi}function checkFirstPlayerAdv () {if [ $2 -gt $3 ]; thenecho "$1: Advantage"fi}

This’ll work for now. Let’s test the advantage for the second player:

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

And the code:

function displayScore () {if outOfRegularScore $2 $4 ; thencheckEquality $2 $4checkAdvantage $1 $2 $3 $4elseecho "$1: `convertToTennisScore $2` - $3: `convertToTennisScore $4`"fi}function checkAdvantage () {if [ $2 -gt $4 ]; thenecho "$1: Advantage"elif [ $4 -gt $2 ]; thenecho "$3: Advantage"fi}

This works, but we have some duplication in the checkAdvantage function. Let’s simplify it and call it twice:

function displayScore () {if outOfRegularScore $2 $4 ; thencheckEquality $2 $4checkAdvantage $1 $2 $4checkAdvantage $3 $4 $2elseecho "$1: `convertToTennisScore $2` - $3: `convertToTennisScore $4`"fi}function checkAdvantage () {if [ $2 -gt $3 ]; thenecho "$1: Advantage"fi}

This is actually better than our previous solution, and it reverts to the original implementation of this method. But we now we have another problem: I feel uncomfortable with the $1, $2, $3 and $4 variables. They need meaningful names:

function displayScore () {firstPlayerName=$1; firstPlayerScore=$2secondPlayerName=$3; secondPlayerScore=$4if outOfRegularScore $firstPlayerScore $secondPlayerScore; thencheckEquality $firstPlayerScore $secondPlayerScorecheckAdvantageFor $firstPlayerName $firstPlayerScore $secondPlayerScorecheckAdvantageFor $secondPlayerName $secondPlayerScore $firstPlayerScoreelseecho "$1: `convertToTennisScore $2` - $3: `convertToTennisScore $4`"fi}function checkAdvantageFor () {if [ $2 -gt $3 ]; thenecho "$1: Advantage"fi}

This makes our code longer, but it is significantly more expressive. I like it.

It’s time to find a winner:

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

We only have to modify the checkAdvantageFor function:

function checkAdvantageFor () {if [ $2 -gt $3 ]; thenif [ `expr $2 - $3` -gt 1 ]; thenecho "$1: Winner"elseecho "$1: Advantage"fifi}

We are almost done! As our last step, we’ll write the code in tennisGame.sh to make the acceptance test pass. This will be fairly simple code:

#! /usr/bin/env sh### tennisGame.sh ###. ./functions.shplayersLine=`head -n 1 $1`echo "$playersLine"firstPlayer=`getFirstPlayerFrom "$playersLine"`secondPlayer=`getSecondPlayerFrom "$playersLine"`wholeScoreFileContent=`cat $1`totalNoOfLines=`echo "$wholeScoreFileContent" | wc -l`for currentLine in `seq 2 $totalNoOfLines`dofirstPlayerScore=$(getScoreFor $firstPlayer "`echo \"$wholeScoreFileContent\" | head -n $currentLine`")secondPlayerScore=$(getScoreFor $secondPlayer "`echo \"$wholeScoreFileContent\" | head -n $currentLine`")displayScore $firstPlayer $firstPlayerScore $secondPlayer $secondPlayerScoredone

We read the first line to retrieve the names of the two players, and then we incrementally read the file to compute the score.


Final Thoughts

Shell scripts can easily grow from a few lines of code to a few hundred of lines. When this happens, maintenance becomes increasingly difficult. Using TDD and unit testing can greatly help to make your complex script easier to maintain—not to mention that it forces you to build your complex scripts in a more professional manner.


Original Link: http://feedproxy.google.com/~r/nettuts/~3/8e2tSC6ql7w/

Share this article:    Share on Facebook
View Full Article

TutsPlus - Code

Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.

More About this Source Visit TutsPlus - Code