Bats is a well established contender in the scene of Bash testing frameworks. It has many features and provides a custom @test
annotation to make tests easier to write.
✔️ Test files are "almost Bash".
.bats
extension. This is not a huge problem, but the editor has to be adapted
a bit to offer syntax highlight and the usual features for these files.
✔️ Also, tests are marked with a special @test
annotation, which is not Bash syntax. It's is picked up by BATS which
translates it into a test wrapper. Although it's special syntax, it makes really clean how tests can be defined:
@test "ice cream price should be 100 per portion for low quantities" {
run src/ice_cream_price.sh 1
assert_success
assert_output -p "Total 100"
}
As can be seen in this example, it also offers a special run
construct to run the script under test and capture the results.
Finally, although it's optional, BATS test files can have their own shebang line as well: #!/usr/bin/env bats
.
✔️ By default it runs all test files in the given directory, or just a given file. It can also consider subdirectories
with the --recursive flag
. With --filter
one can specify a regex, and only tests with a matching name will be executed.
@test "addition using bc" {
result="$(echo 2+2 | bc)"
[ "$result" -eq 4 ]
}
All the examples promote this approach. In my opinion, it has two serious problems:
- one have to be fluent in Bash conditionals in order to work with it
- the report lacks important information, like the expected and actual values
✗ addition using bc
(in test file test/test2.bats, line 8)
`[ "$result" -eq 4 ]' failed
# what was the $result? :(
✔️ It has an extension called bats-assert that provide assertions like equals or partial matching on the output. They provide nice error messages too:
load '../lib/bats-support/load'
load '../lib/bats-assert/load'
@test "using asserts" {
run src/ice_cream_price.sh 4
assert_success
assert_output -p "Total 500"
}
✗ using asserts
(from function `assert_output' in file test/../lib/bats-assert/src/assert_output.bash, line 186,
in test file test/asserts.bats, line 7)
`assert_output -p "Total 500"' failed
-- output does not contain substring --
substring : Total 500
output : Total 400
--
bats-assert
are working directly with the return code or the output
of the command executed with run. There are not too many general assertions besides assert_equal
.
✔️ It's possible to create custom assertions, and also there are some dedicated constructs in the framework to support creating them:
- With
load
shared test code (e.g. assertion functions) can be imported (doc) - The bats-support library provides common functions for error reporting and output formatting.
✓ this is the first test from the first test suite
✓ this is the second test from the first test suite
✓ this is the first test from the second test suite
✓ this is the second test from the second test suite
✔️ Mocking is possible with all three common techniques:
- alias
- function export
- PATH override
✔️ Bats itself can source
scripts that set custom Bash options, like -e
or -u
without breaking test
execution or causing problems for the test framework.
[1],
[2].
✔️ Because a non-zero exit code for any test command results in a failed test, I thought that all unit tested functions
are inherently run with the errexit
mode set. Luckily this is not the case. The run
construct
provides a sandbox to the function under test.
nounset
will work normally. However, for some reason when a function is exercised with the run
construct the
-e
option is always forcefully is disabled. So, if you design a script that use
set -e
and unit test its functions with Bats, in the unit tests the -e
will not apply and the function
might behave differently.
For more details, see the example tests that exercise scripts that use custom Bash options.