Custom asserts
Custom assertions let you extend bashunit with your own reusable checks, ideal for domain-specific assertions that don't need to live in the core library.
TIP
Check the internal functional tests: tests/functional/custom_asserts_test.sh (link)
Assertion behavior
When using the bashunit facade, assertions automatically respect the guard behavior: if a previous assertion in the same test already failed, subsequent assertions are skipped. This matches popular testing libraries default behavior.
This is the default. -R/--run-all (BASHUNIT_STOP_ON_ASSERTION_FAILURE=false) disables the guard, so every assertion inside your custom assertion runs and counts even after an earlier failure.
Test name detection
Custom assertions automatically display the correct test function name in failure messages, not the custom assertion name. This makes it easy to identify which test failed, even when using deeply nested custom assertions.
API Reference
assert_that
bashunit::assert_that <expected> <actual> <cmd> [args...]
Runs cmd and marks the assertion passed or failed accordingly, in a single call.
| Parameter | Description |
|---|---|
expected | What the assertion expects, as shown in the failure block |
actual | The actual value received |
cmd [args...] | The command deciding the verdict: exit 0 passes, anything else fails |
Returns 0 when the command succeeds and 1 when it fails, so it can be chained. Leaving it as the last statement of your custom assertion is fine: a failed assertion is reported once, as a failure.
The command is invoked directly, without eval, so arguments keep their word boundaries and nothing is re-parsed by the shell.
assert_once
bashunit::assert_once <label?> <actual?>
Declares that the calling custom assertion counts and reports as one assertion, whatever it asserts internally. See Composing with existing assertions.
| Parameter | Description |
|---|---|
label | What the assertion expects, shown in the failure block. Omit it to report the innermost failure message instead |
actual | The actual value shown against that label |
assertion_failed
bashunit::assertion_failed <expected> <actual> <failure_condition_message?> <label?>
Marks the current assertion as failed and prints a failure message.
| Parameter | Description |
|---|---|
expected | The expected value |
actual | The actual value received |
failure_condition_message | Optional message describing the failure condition (default: "but got") |
label | Optional name shown in the failure block (default: the test function name) |
assertion_passed
bashunit::assertion_passed
Marks the current assertion as passed. Call this when your custom assertion succeeds.
fail
bashunit::fail <message?>
Marks the current assertion as failed and prints Message: '<message>' instead of the Expected/but-got block. Use it when there is no meaningful expected value.
Examples
One-call assertion
bashunit::assert_that collapses the pass/fail bookkeeping into a single line, so the two counters cannot drift apart:
function assert_positive_number() {
bashunit::assert_that "positive number" "$1" test "$1" -gt 0
}
function test_value_is_positive() {
assert_positive_number 1 # Passes
}
function test_value_is_not_positive() {
assert_positive_number 0 # Fails with: "Expected 'positive number' but got '0'"
}Any command works as the verdict, not only test:
function assert_valid_json() {
bashunit::assert_that "valid JSON" "$1" jq -e . <<< "$1"
}
function assert_file_is_executable() {
bashunit::assert_that "an executable file" "$1" test -x "$1"
}Naming your own failures
By default a failure block is labelled with the test function name. Pass a fourth argument to bashunit::assertion_failed when the assertion should name itself instead:
function assert_http_success() {
local status_code="$1"
if [ "$status_code" -lt 200 ] || [ "$status_code" -ge 300 ]; then
bashunit::assertion_failed "a 2xx status" "$status_code" "but got " "Assert HTTP success"
return
fi
bashunit::assertion_passed
}Basic custom assertion
function assert_foo() {
local actual="$1"
if [ "foo" != "$actual" ]; then
bashunit::assertion_failed "foo" "$actual"
return
fi
bashunit::assertion_passed
}
function test_value_is_foo() {
assert_foo "foo" # Passes
}
function test_value_is_not_foo() {
assert_foo "bar" # Fails with: "Expected 'foo' but got 'bar'"
}Using fail() for simple messages
You can also use bashunit::fail for custom assertions that just need a message:
function assert_valid_json() {
local json="$1"
if ! echo "$json" | jq . > /dev/null 2>&1; then
bashunit::fail "Invalid JSON: $json"
return
fi
bashunit::assertion_passed
}
function test_api_returns_valid_json() {
local response='{"status": "ok"}'
assert_valid_json "$response"
}Composing with existing assertions
Custom assertions can call other bashunit assertions internally:
function assert_http_success() {
local status_code="$1"
assert_greater_or_equal_than "200" "$status_code"
assert_less_than "300" "$status_code"
}
function test_api_returns_success() {
local status_code=200
assert_http_success "$status_code"
}By default each inner assertion is counted and reported on its own, so one call to assert_http_success counts as two assertions and a failure reads Expected '500' to be less than '300' — the internal step rather than "not an HTTP success".
Add bashunit::assert_once at the top to report the whole thing as one:
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"
assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}Tests: 1 passed, 1 failed, 2 total
Assertions: 1 passed, 1 failed, 2 totalThe failure now reads Expected 'a 2xx status' but got '500', labelled with the test that called it.
Parameters: see assert_once.
Notes:
- It is opt-in. A composed assertion without the marker keeps counting and reporting exactly as before.
- Every call counts, including repeated calls from a loop.
- The inner assertions all run even when one fails, so the marker reports on the whole check rather than stopping at the first step.
- The marker is settled when the custom assertion is called again, when an assertion runs after it returned, or at the end of the test — whichever comes first.
bashunit::assert_that(above) is the shorter option when the check is a single command rather than several composed assertions.
Custom assertion with custom failure message
function assert_positive_number() {
local actual="$1"
if [ "$actual" -le 0 ]; then
bashunit::assertion_failed "positive number" "$actual" "got"
return
fi
bashunit::assertion_passed
}Testing your custom assertions
A custom assertion is code, so it deserves tests of its own — including for the case it is meant to reject. Three assertions assert about assertions:
function test_positive_number_accepts_one() {
assert_assertion_passes assert_positive_number 1
}
function test_positive_number_rejects_zero() {
assert_assertion_fails assert_positive_number 0
}
function test_positive_number_says_what_it_wanted() {
assert_assertion_fails_with "positive number" assert_positive_number 0
}The inner assertion runs isolated. Its verdict never lands in the run totals, its failure block never reaches the console, and it cannot trip the stop-on-failure guard for the rest of your test — only the outer assert_assertion_* is counted.
An assertion that counts nothing fails both assert_assertion_passes and assert_assertion_fails, which is what catches a custom assertion that forgot to mark its outcome at all.
To assert on what a failure did not say, read the captured message from $_BASHUNIT_ASSERT_INNER_OUTPUT_OUT — colour-stripped and flattened to a single line:
function test_failure_is_labelled_with_the_test_not_the_assertion() {
assert_assertion_fails assert_positive_number 0
assert_not_contains "Assert positive number" "$_BASHUNIT_ASSERT_INNER_OUTPUT_OUT"
}Loading your assertions once
Sourcing a shared assertions file from set_up re-runs it for every test, and only in the file that does it. Load it once for the whole run with a bootstrap file instead:
./bashunit --boot tests/bootstrap.sh tests/# .env or bashunit.env
BASHUNIT_BOOTSTRAP="tests/bootstrap.sh"Your tests/bootstrap.sh then sources the assertions:
source "$(dirname "${BASH_SOURCE[0]}")/custom_asserts.sh"See Configuration and Command line for the full bootstrap options.
Listing your assertions
bashunit doc prints the built-in catalogue. Once a bootstrap defines your own assertions, it appends them too, rendering the comment block above each one:
./bashunit doc --boot tests/bootstrap.sh # built-ins, then a "Custom assertions" section
./bashunit doc --custom --boot tests/bootstrap.sh # only your own
./bashunit doc --custom --boot tests/bootstrap.sh http # ...narrowed by a filter## assert_http_success
--------------
Asserts that the status code is a 2xx.
Arguments: $1 - the status codeThe whole comment run above the function is printed, so a two-line docstring renders as two lines. A function with no comment block prints its heading and the divider with nothing under them.
With BASHUNIT_BOOTSTRAP set, the --boot flag can be omitted. A bootstrap is required either way: it is the only point at which your assertions are guaranteed loaded, which is another reason to prefer it over sourcing from set_up.
Write the docstring as a plain comment block immediately above the function — the same shape the built-ins use:
# Asserts that the status code is a 2xx.
# Arguments: $1 - the status code
function assert_http_success() {
bashunit::assert_once "a 2xx status" "$1"
assert_greater_or_equal_than "200" "$1"
assert_less_than "300" "$1"
}Best practices
Prefer
bashunit::assert_that: one call marks the assertion passed or failed, so you cannot forget thereturnafter a failure (which would bump both counters) or forgetbashunit::assertion_passed(which would leave the test with zero assertions, reported as risky).Always return after failure: when writing the long form by hand, call
returnafterbashunit::assertion_failedorbashunit::failto stop execution of your custom assertion.Always mark success: call
bashunit::assertion_passedwhen your assertion succeeds.Use descriptive names: Name your custom assertions clearly, e.g.,
assert_valid_email,assert_file_contains_header.Keep assertions focused: Each custom assertion should test one specific condition.
Related
- Assertions — the built-in assertion reference
- Globals —
bashunit::helper functions - Common patterns — real-world testing patterns