Unit testing in Bash
Bats
Bats is a TAP- a compliant testing framework for Bash. It provides a simple way to verify that the UNIX programs you write behave as expected.
A Bats test file is a Bash script with special syntax for defining test cases. Under the hood, each test case is just a function with a description.
#!/usr/bin/env bats
@test "addition using bc" {
result="$(echo 2+2 | bc)"
[ "$result" -eq 4 ]
}
@test "addition using dc" {
result="$(echo 2 2+p | dc)"
[ "$result" -eq 4 ]
}
Bats is most useful when testing software written in Bash, but you can use it to test any UNIX program.
Test cases consist of standard shell commands. Bats makes use of Bash's errexit (set -e) option when running test cases. If every command in the test case exits with a 0 status code (success), the test passes. In this way, each line is an assertion of truth.
Installation
sudo apt install bats
Running a file
Bats will be installed as a system binary, so you can run the tests as follows:
bats ./my_test.bats
Or you can make it an executable as long as you have the following header:
#!/usr/bin/env bats
Tested file considerations
In order to design for testability, the file should be divided into several functions. So that unit tests can target each of these. If there is code that needs is executed by the script when called, but is not necessary for the test, the code should be wrapped in a if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
block.
check_l4t_folder () {
if [[ -f ${l4t_dir}/flash.sh ]]; then
msg "Found all required files"
else
msg "Missing files" && exit 1
fi
return 0
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
check_l4t_folder
fi
The previous code block if run directly from an executable will run the check_l4t_folder function, when sourced it will only declare the function which can be used for testing, but not run it immediately.
Checking for return value
In the following code snippet, you'll see two ways of checking for return values. The first will check for the actual return value of the function and its side effects. The second will check for the output of the function (done through a mocked exit function).
###############
# Source file #
###############
die() {
local msg=$1
local code=${2-1} # default exit status 1
msg "$msg"
exit "$code"
}
parse_params() {
while :; do
case "${1-}" in
-h | --help) usage ;;
--select_active_partition)
if [[ "${2-}" != "A" && "${2-}" != "B" ]]; then
die "Invalid argument for ${1-}: ${2-}. Should be either A or B"
fi
new_active_partition="${2-}"
shift
;;
-?*) msg "Unknown option: $1" && usage;;
*) break ;;
esac
shift
done
return 0
}
#############
# Test file #
#############
exit() {
echo $1
}
@test "check_parse_params_select_active_ok" {
parse_params --select_active_partition A
# We check the return value by calling $?, this value comes from a "result" call in a function
ret=$?
[ "$ret" == 0 ]
# Functions called outside of a () block will have side effects on the parent function,
# as such we can check for any variables it set
[ "$new_active_partition" == "A" ]
}
@test "check_parse_params_select_active_wrong_part" {
ret=$(parse_params --select_active_partition C)
# Since we are mocking, the exit will return through an echo, in this case we can
# check the function result.
[ "$ret" != 0 ]
}
Mocking binaries
Mocking binaries is a useful technique to avoid unwanted side effects on the system where the test is being run. This for example can avoid the need for superuser privileges or the existence of a given binary.
Mocking system binaries is quite straightforward, just declare a function with the same name as the binary. These mocked binaries will even propagate to the sourced file and not only the test one.
If the file to be mocked is not a system binary, the script should be designed with the ability to have relative paths that can be changed in mind.
###############
# Source file #
###############
run_flash () {
pushd ${l4t_dir}
sudo ./flash.sh jetson-tx2 mmcblk0p1
popd
}
#############
# Test file #
#############
# Here we mock the sudo utility, we don't need to check for privileges, but still need it to call the argument as a function.
sudo() {
$1
}
@test "run_flash_ok" {
source $SOURCE_SCRIPT
# We use the mktemp utility to create everything in a temporary directory
l4t_dir=$(mktemp -d)
# The source file needs a flash.sh script, here we create a binary and make it an executable
cat > ${l4t_dir}/flash.sh << EOF
#!/usr/bin/env bash
echo 0
EOF
chmod +x ${l4t_dir}/flash.sh
# This is the actual function test
ret=$(run_flash)
[ "$ret" == 0 ]
}
For direct inquiries, please refer to the contact information available on our Contact page. Alternatively, you may complete and submit the form provided at the same link. We will respond to your request at our earliest opportunity.
Links to RidgeRun Resources and RidgeRun Artificial Intelligence Solutions can be found in the footer below.