I have started writing tests for one of the scripts I use regularly. I started refactoring the script, and then decided to do it again so I didn’t have to do all the testing manually!
Here’s some things I’ve learned.
Test functions: make your code so it can be sourced
I think I’m starting to understand why code I’ve seen written in other languages is so modular.
I think the testing frameworks (and adjacent functionality like code coverage) assume that tests will be unit testing small contained bits of functionality, and BATS requires that your code file can be sourced.
The core docs didn’t provide a good way to get stared, but I lucked out with an opensource.com article.
Take all the code that is executed in the main part of the script and move it into a function, called something like run_main. Then, add the following to the end of the script:
if [[ "${BASH_SOURCE[0]}" == "${0}" ]] then run_main fi
That is, before:
#!/bin/bash
echo "hello world"
After:
#!/bin/bash
run_main() {
echo "hello world"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]
then
run_main
fi
As shown, the script has been made BATS compatible ‘quick and dirty’ – without even indenting the code. The goal is simply to get started with writing tests.
Then you can refactor:
#!/bin/bash
run_main() {
say_hello
}
say_hello () {
echo "hello world"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]
then
run_main
fi
And now you have a function that can be tested.
dirname/basename in a post-BATS world
I routinely use code like basename $0 and dirname $0. This breaks the tests, as $0 is replaced with some part of the BATS framework. A replacement is needed.
There’s a reference to this in the BATS docs recommending the use of $BATS_TEST_FILENAME rather than $0 or ${BASH_SOURCE[0]}.
That works when the code is being tested by BATS, but not when its being executed normally. I am using ${BASH_SOURCE[0]} since that worked in my testing.
script_name=$(basename ${BASH_SOURCE[0]})
Anchoring directory references
A way to refer (in the test code) to locations adjacent to the code and the test code is needed. I needed this to work when was running BATS interactively and in a CI pipeline.
I use ${BATS_TEST_DIRNAME} for this. It’s kind of like dirname $0 except that BATS supplies it to you for free.
Some of my functions take files as parameters, and validate that the file exists. So, I want to supply a valid file that can pass the test.
(I also planned ahead and checked in some test files. The code I’m refactoring is working with EXIF data in images.)
@test "check_args: function valid param" {
run check_args ${BATS_TEST_DIRNAME}/../test/assets/PA200004.JPG
[ "$status" -eq 0 ]
}
@test "check_args: function non valid param" {
run check_args ${BATS_TEST_DIRNAME}/../test/assets/PA200004xx.JPG
[ "$status" -eq 1 ]
}
Rough structure of my project:
mycode.bash
test/mycode.bats
test/assets/PA200004.JPG
I don’t really know why I’m using ${BATS_TEST_DIRNAME}/../ but that does have the advantage of making the root of the project be the baseline for all references to files.
Script filenames
I tend to name my scripts with .sh file extension, habit from starting out on AIX (where actually I think I’d have used .ksh) and Solaris.
BATS assumes .bash file extension for the code it is testing.
#!/usr/bin/env bats
load ../mycode
Testing STDOUT and STDERR
I tend to emit errors to STDERR, using echo "some error" 1>&2. Testing that the right error was emitted was not obvious since all the examples I found only checked STDOUT:
@test "say_hello: says hello" {
run say_hello
[ "$status" -eq 0 ]
[ "$output" = "hello world" ]
}
To get STDERR as well:
@test "say_hello: says hello" {
run --separate-stderr say_hello
[ "$status" -eq 0 ]
[ "$output" = "hello world" ]
[ "$stderr" = "" ]
}
Debug output
When a test fails, it’s really useful for it to emit some output for debugging purposes.
I’ve found that this needs to be the first check after run, otherwise no debug output.
@test "say_hello: says hello" {
run --separate-stderr say_hello
[ "x$BATS_TEST_COMPLETED" = "x" ] && echo "o:'${output}' e:'${stderr}'"
[ "$status" -eq 0 ]
[ "$output" = "hello world" ]
[ "$stderr" = "" ]
}
Leave a comment