Skip to content
Kate Ward edited this page Oct 24, 2021 · 11 revisions

Asserts

Using command output in assert message.

Let's say a function was called, and the output should be included to help in debugging the test. This is the recommended way to do so.

#!/bin/bash
# test_good_bad.sh
  
good() { echo "Wohoo!!"; }
bad() { echo "Booo :-("; return 123; }

test_functions() {
  # Naively hope that all functions return true.
  while read desc fn; do
    output=$(${fn} 2>&1); rtrn=$?
    assertTrue "${desc}: unexpected error (${rtrn}); ${output}" ${rtrn}
  done <<EOF
good() good
bad()  bad
EOF
}

. ~/lib/sh/shunit2

Testing the code show that the bad output is added to the assertTrue output.

$ ./test_good_bad.sh 
test_functions
ASSERT:bad(): unexpected error (123); Booo :-(

Ran 1 test.

FAILED (failures=1)

Verifying command output

The most straightforward method is to pipe STDOUT and STDERR to files, and use the diff or grep command against those files. To simplify this, shUnit2 provides a temporary directory for writing such files that will be automatically cleaned up at the end of the test run.

The minimal solution for this looks like this. For a slightly more helpful example, see examples/output_test.sh.

#! /bin/sh

generateOutput() {
  echo 'this output went to STDOUT'
  echo 'this output went to STDERR' >&2
  return 1
}

testGenerateOutput() {
  ( generateOutput >"${stdoutF}" 2>"${stderrF}" )
  assertTrue "the command exited with an error" $?

  # This test will pass because the grepped output matches.
  grep -q 'STDOUT' "${stdoutF}"
  assertTrue 'STDOUT message missing' $?

  # This test will fail because the grepped output doesn't match.
  grep -q 'ST[andar]DERR[or]' "${stderrF}"
  assertTrue 'STDERR message missing' $?

  return 0
}

oneTimeSetUp() {
  # Define global variables for command output.
  stdoutF="${SHUNIT_TMPDIR}/stdout"
  stderrF="${SHUNIT_TMPDIR}/stderr"
}

setUp() {
  # Truncate the output files.
  cp /dev/null "${stdoutF}"
  cp /dev/null "${stderrF}"
}

# Load and run shUnit2.
. ./shunit2

Writing custom asserts

The following code is pulled from the unit tests of log4sh, and demonstrates the testing of various conversion patterns. The custom assert was written to reduce code duplication (think the DRY principle) for many tests in that codebase.

assertPattern() {
  msg=''
  if [ $# -eq 3 ]; then
    msg=$1
    shift
  fi
  pattern=$1
  expected=$2

  appender_setPattern ${APP_NAME} "${pattern}"
  appender_activateOptions ${APP_NAME}
  actual=`logger_info 'dummy'`
  msg=`eval "echo \"${msg}\""`
  assertEquals "${msg}" "${expected}" "${actual}"
}

testCategoryPattern() {
  pattern='%c'
  expected='shell'
  msg="category '%c' pattern failed: '\${expected}' != '\${actual}'"
  assertPattern "${msg}" "${expected}" "${pattern}"
}

Misc

Passing arguments to test script.

Passing command-line arguments to a test script is not a problem, as long as one small requirement is met. Before calling shunit2, use shift to shift all the command-line arguments off the stack.

#!/bin/bash
# test_date_cmd.sh
  
# Echo any provided arguments.
[ $# -gt 0 ] && echo "ARGC: $# 1:$1 2:$2 3:$3"

test_date_cmd() {
  ( date '+%a' >/dev/null 2>&1 )
  local rtrn=$?
  assertTrue "unexpected date error; ${rtrn}" ${rtrn}
}

# Eat all command-line arguments before calling shunit2.
shift $#
. ~/lib/sh/shunit2

Calling the command without arguments.

$ ./test_date_cmd.sh 
test_date_cmd

Ran 1 test.

OK

Calling the command with arguments.

$ ./test_date_cmd.sh a b c
ARGC: #:3 1:a 2:b 3:c
test_date_cmd

Ran 1 test.

OK

Writing tests for existing commands

Writing unit tests for a pre-existing binary is not much different than writing unit tests for functions. The only real difference is that you do not have the degree of flexibility of being able to rewrite the command to make testing of unique conditions easier.

If you would like to see an example of testing the mkdir command, look examples/mkdir_test.sh.

Writing unit tests that mock files

Mocking files is not difficult, but it usually requires slightly refactoring the code under test so that the code isn't only using constants to specify the files to be read. To alternatives are:

  • The filename to be read should be passed into a function that reads the file, allowing a unit test to pass a different file in.
  • A function should be provided that returns the filename to be read, and that function returns a filename according to whether the code is functioning normally, or under test.

Example code for both of these can be seen in examples/mock_file.sh and examples/mock_file_test.sh

Third-party packages

macOS

Homebrew

https://brew.sh/

Install shunit2.

$ brew install shunit2

Create a basic homebrew_test.sh test.

testHomebrew() {
  assertTrue 0
}
. /usr/local/bin/shunit2

Run the test.

$ sh homebrew_test.sh
testHomebrew

Ran 1 test.

OK