Test driven design in Bash

To test your bash scripts, or to use Bash to test other scripts, you need a way to create tests quickly and easily. I created a library to support testing in bash. I have used this library to test Bash, Python, and Perl scripts.

An updated version of the source code is available on GitHub.

One of the example tests uses a Mock Module technique. Simply put, the test creates a Bash function called “mysql” and exports it. The function returns a useful test example that could be returned by the database in question. This technique relies on knowing what your script expects from “mysql”: in other words, this is for white box testing, not black box.

Basically, the goal of the test scripts is to run some tests on your application/script and report:

  • What tests were run
  • What tests failed
  • A summary:
    • Failure notes with names of output files for convenience
    • How many tests failed
    • How many tests were run

The test name is used several times in this process, so I abstracted it into a variable. For instance, I use it to report what test is running, and when summarizing failure results.

The goal of creating the library was to abstract away all the lines in the script that I repeated for every test. This makes the tests readable and simple tests very fast to create.

Here is the code for the library. The code for a test follows:

function init {
    # initialize variables
    TESTCOUNT=0
    TESTPASS=0

    PROGRAM="$1"
    PROGRAM_NAME=$(basename $PROGRAM)

    # "mesage" is set by "check" function on failure
    # and printed in summary
    message=""

    # print initial state to the screen
    echo -n "Start date:  "
    date

    STARTDIR=`pwd`
    echo -n "PWD  $STARTDIR"
    echo

    # "t" - name of the test directory
    # - where we write test output and compute repeatable versions
    #   of the output to compare with expected output
    t=test-out-$PROGRAM_NAME

    # start reporting
    echo
    echo "This script tests $PROGRAM"
    echo

    # clean out old diff files
    clean
}

function clean {
    rm -f ./$t/*out
    rm -f ./$t/*comp
}

# optional check - most of my tests don't need this
function user_check {
    req_user=$1
    # only one user can run this test script and have it work
    # - due to file permissions, etc.
    user=`whoami`
    if [ "$user" != "$req_user" ]
    then
        echo
        echo "  Error: You must only run this script as user \"$req_user\", not \"$user\"."
        echo
        exit 1
    fi
}

function test_init {
    # do several common tasks required by all tests
    test="$1"
    let TESTCOUNT=$TESTCOUNT+1

    echo `date +%T` - $1
    $1
}

function check {
    # Here are the common tasks which happen after the test is run.

    # grab input
    mytest=$1
    result_desired=$2
    myresult=$3

    # result info
    if [ "$result_desired" = "true" ]
    then
        # expecting 'true' = 0
        cmp=-ne
        fail_text="false"
    else
        # expecting 'false' != 0
        cmp=-eq
        fail_text="true"
    fi

    # check the return value
    if [ $myresult $cmp 0 ]
    then
        message="$message\n'$test' - $PROGRAM unexpectedly returned $fail_text"
        return
    fi

    # "clean_output" is a call back function - it needs to be present in the test itself
    # - clean the output of the test for easy comparison to "expected" file
    clean_output $t/$mytest.out $t/$mytest.comp

    # compare test output
    if diff --ignore-all-space --ignore-blank-lines --brief $t/$mytest.expected $t/$mytest.comp
    then
        # should only reach here if return value and diff both pass
        let TESTPASS=$TESTPASS+1
        # in case of diff failure, "diff" gives adequate message
    fi
}


function summary {
    echo
    echo -e "$message"
    echo
    echo "-- Test summary"
    printf "Tests passed: %2d\n" $TESTPASS
    printf "Total tests:  %2d\n" $TESTCOUNT
    echo
    echo -n "End date:  "
    date
    exit
}

# run single test
function single_test {
    echo "Checking input $1 is a bash function (tests are functions)"
    msg=`type $1 | grep "is a function" | wc -l`
    if [ "$msg" -gt 0  ]
    then
        test_init $1
    else
        echo Function $1 not found.
    echo Here is a list of functions, including tests:
    echo
    grep ^function $0
    fi
    summary
}

Here is the code for a test that would use the library:

#!/usr/bin/env bash

source ../../testlib/testlib.sh

init ../your-script

# callback function required by the test lib
function clean_output {
    in=$1
    out=$2

    # replace Qual ID's and LSF ID's with 6 hashes
    # replace dates with ##-##-##
    # replace times with ##:##
    # replace username with "user        "
    cat $1 | \
    sed -r -e 's/[[:digit:]]{7,}/#######/' \
           -e 's/[[:digit:]]{1,2}-[[:digit:]]{1,2}-[[:digit:]]{1,2}/##-##-##/' \
           -e 's/[[:digit:]]{1,2}:[[:digit:]]{1,2}/##:##/g' \
           -e 's/[[:digit:]]{1,2}h [[:digit:]]{1,2}m/##h ##m/' \
           -e "s/^$USER.+/user/"  > $2
    return
}


# --- end of common info required in every test script

# --- test functions

function no_param {
    $PROGRAM > $t/$test.out 2>&1
    result_val=$?

    check $test false $result_val
}

function mock_db {
    # Here we create mock access to the database.
    # The script is a shell script that uses "mysql".
    # The function is defined where the script is run,
    # in a subshell, ie in brackets (), so as not to pullute
    # the main script environment with the function.
    # This might not be necessary inside a function, but I didn't
    # take time to test it.
    (
    function mysql {
        # mock database
        # - tab separated output, just like mysql gives
        echo -e "1234567\tPENDING\totheruser"
    }
    export -f mysql

    $PROGRAM 1001 >$t/$test.out 2>&1
    )
    result_val=$?

    check $test true $result_val
}


# --- the rest is info required in every script

# ---  run only one test if passed on command line
[ $# -gt 0 ] && single_test "$@"

# run the tests
test_init no_param
test_init mock_db

# wrap up
summary
Advertisements

One response to “Test driven design in Bash

  1. Pingback: TDD in shell scripts | Wisdom's Quintessence

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s