Bait(Bash Automated Integration Test) is a small test framework in Bash.
It produces TAP style test outputs, and unlike most of the test tools in Bash, it doesn't require you to run every test in its own subprocess.
Suggestions, bug reports, or pull requests are most welcomed!
# --- FILE: t/example.t --------
#!/usr/bin/env bait
test_data1 () { local data=42; }
test_data2 () { local data=69; }
@setup test_data1
@setup test_data2
Case "An Example Test Case Definition" {
[[ $data -ge 42 ]]
}
Case "Another example with assert checks" {
assert (( 1 + 1 == 2 ))
assert {
: Check if www.google.com is up
local url=https://www.google.com/
[[ $(curl -I -L -sf -w '%{http_code}\n' "$url" | tail -1) == 200 ]]
}
}
# -----------------------------
$ prove -ve bait t/example.t
t/example.t ..
1..3
ok 1 - An Example Test Case Definition
ok 2 - An Example Test Case Definition
ok 3 - Another example with assert checks
ok
All tests successful.
Files=1, Tests=3, 0 wallclock secs ( 0.02 usr 0.00 sys + 0.05 cusr 0.03 csys = 0.10 CPU)
Result: PASS
See bait -h
for useful CLI options.
When run using prove
, CLI options for bait
can be passed after a ::
, like this:
$ prove -e bait t/example.t :: -t1,2
For examples test cases see the documentation below, and take a look at bait.t.
You define test cases using functions provided by Bait.
A test case definition starts with the Case
command like this:
Case "test case description here..., it's optional though."
Then you define a Do()
function, which will be converted to a test
function that represents your test case declared above it. Finally,
to complete the test case definition, you end it with the End Case
command. The whole thing usually looks like this:
Case "Test case description"; Do () {
# commands here will be run when the test case is run.
echo Hello World
(( 1 + 1 == 2 ))
# Note: In Bash, the return status of a function is the status
# of the last command run in it, unless you return explicitly.
}
End Case # Note: You can't nest test case defintions.
Notice that this is all written in Bash, and your test case is just
a Bash function defined by the Do()
function. Also, note that if you define the
Do()
function using (
and )
instead of {
and }
for the body, then it
will be run in a subshell, which might work better for you if you'd like better
isolation between different test case runs.
The return status of your test function determines the success(ok
) and
failure(not ok
) of your test case when it's run. 0
indicates success; other
values indicate failure.
Sometimes you'd like to perform several checks in one test case. Bait
provides a check
function to help you do that. Example:
Case "An assert-check example"; Do () {
assert () { true; }; check
assert () { false && true; }; check # this check fails
assert () { true || false; }; check
}
End Case
Basically, you define an assert()
function containing your check logic,
and then you call check
immediately after the functinon definition.
The above example will run all three checks and result in a failed test case
because the second assert check fails.
Stdout and stderr of a test case are captured, and will be shown at the end of all tests runs along with failed asserts for the failed test cases.
The advantage of defining your check in an assert()
function like this is that
you can put anything in it easily without messing with quotes. Plus, when a check
fails, Bait will show the source code of its assert()
function for you to see.
In your test case function, you can use the SKIP
command to skip the test case.
Example:
Case "Skipping a Test"; Do () {
echo "About to skip this test case..."
SKIP "you can provide a reason here..., it's optional."
echo "Execution won't reach here."
}
End Case
However, when using SKIP
in a loop directly or indirectly, you need to specify
an extra integer argument for it. See here for more details.
Similarly, you can also mark a test case as TODO, using the TODO
command.
Example:
Case "Test someting"; Do () { TODO; }; End Case
NOTE: Unlike SKIP
, TODO
won't change the execution flow
Now, defining test cases using Case
, Do()
, and End Case
, like above, as well
as writing assert() { ... ; }; check
, can be tedious. Therefore, Bait by default
preprocesses your test script and turn test cases and asserts written like below into
valid test case definitions like above. Here's a table illustrating the transformations:
Before | After |
---|---|
Case { ...; } | Case; Do () { ...; }; End Case |
Case { ... } |
Case; Do () { ... } End Case |
Case ... { ...; } | Case ...; Do () { ...; }; End Case |
Case ... { ... } |
Case ...; Do () { ... } End Case |
assert ... | assert () { ...; }; check |
assert "..." { ... } |
assert () { : "..." ... } check |
One caveat with preprocessing is that it can't 100% get it right in all cases. Particularly,
it requires you to indent your closing }
or )
correctly to match the starting Case
or assert
.
However, in practice, this is usually not a problem if you have set up auto-indent in your text editor.
NOTE: With the
-n
(don't run tests) option, you can see how Bait preprocessed your test script. Preprocessing can be turned off via the-P
option.
A test fixture is a function that prepares the test data(in the form of local or global variables), or external resources(e.g., files or database) to be used by other test cases. For example:
my_series() {
local series=(2 3 5 7 11 13 17)
}
@setup my_series
Case "Sum of the series must be greater than 42" {
local i sum=0
for i in ${series[*]}; do
(( sum += i ))
done
(( sum > 42 ))
}
You use @setup
to "decorate" a test case with the fixture functions it needs,
and Bait will take care of setting up the call chain so that the test data
set up by the fixtures will be available to your test case function.
Notice that in the test case, it's using series
, which is a local variable
in the fixture. This is possible because of dynamic scoping in Bash.
You can chain multiple fixtures with @setup fixture1 fixture2 ...
, and Bait will
call your test case function like this: fixture1 -> fixture2 -> ... -> test case
.
This also means local variables in fixture1
will be available in fixture2
, and
local variables in fixture2
will be available in fixture3
, and so on..., and
all of them will be available in your test case.
You can also decorate your test case with multiple @setup
chains, and Bait will
create one test case for each chain. This could be useful when you have a test
case, but need it to be run with different test data setups, for example.
You can define as many test cases as you want, and you can put them in different files organized under different directories.
To run your test cases with Bait, first make sure the bait
script is in your
PATH
. One way to run a test script is to make your test script self-executable
by using the #!/usr/bin/env bait
shebang line, chmod +x
the script, and then
just run it.
If you have many test scripts, another way to run them is to use the prove
utility, which comes with perl so you probably already have it installed. It should
be invoked with -e bait
, for example:
# Run all test cases under the t/ directory
$ prove -e bait
See prove
man page for more details.