diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f7a997..18fc0a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Deprecated forms now warn at runtime on stderr; silence with `BASHUNIT_NO_DEPRECATION_WARNINGS=true` (#866) - `bashunit::state::print_line` / `print_tap_line` moved to `bashunit::console_results::*`; no alias kept (#868) - `bashunit doc` and the [Assertions](https://bashunit.com/assertions) page now cover all 71 assertions, with a quick-reference table +- Docs: `bashunit::unmock` and the lifecycle of a double — automatic per-test cleanup, `set_up_before_script` doubles, and what `unmock` reaches (#899) - Docs: invalid-input handling, `BASHUNIT_REPORT_TAP`/`BASHUNIT_REPORT_JSON`, the standalone exit code, and the optional `["snapshot_file"]` argument - The docs sidebar outline now lists `h3` headings - The `--parallel` unsupported-OS warning no longer claims Alpine is excluded diff --git a/docs/ai-agents.md b/docs/ai-agents.md index 9e32b40c..3c89820c 100644 --- a/docs/ai-agents.md +++ b/docs/ai-agents.md @@ -111,6 +111,10 @@ actually make against this API: - Use `$(bashunit::temp_file)` / `$(bashunit::temp_dir)` for scratch files — they are cleaned up automatically and are safe under `--parallel`. - Never call the network in a test. Use `bashunit::mock` / `bashunit::spy` instead. +- Doubles are torn down after every test — do not add `bashunit::unmock` to `tear_down`. + Use it inside a test to get the real command back, or to suspend a + `set_up_before_script` double for that one test (it cannot undo it for the others, + since each test runs in its own subshell). - Spy assertion argument order is inconsistent — check, don't guess: `assert_have_been_called_times ` but `assert_have_been_called_with [call_index]`. diff --git a/docs/public/bashunit-skill.md b/docs/public/bashunit-skill.md index 92da8e67..28abdb52 100644 --- a/docs/public/bashunit-skill.md +++ b/docs/public/bashunit-skill.md @@ -200,6 +200,7 @@ bashunit::mock date echo "2024-05-01" # replace behaviour bashunit::mock uname <<< "Linux" # heredoc form ignores the call's arguments bashunit::mock curl 1 # all-digits arg = exit code, no output (as for spy) bashunit::spy send_email # record calls, keep behaviour +bashunit::unmock date # restore the real command for the rest of this test assert_have_been_called send_email assert_have_been_called_times 2 send_email # count FIRST, then spy @@ -214,6 +215,11 @@ The argument order is inconsistent between `_times` and `_with`. A swapped pair the assertion* rather than erroring, so it reads like a real defect — check this list rather than guessing. +A double declared inside a test is removed when that test ends, so never unmock in +`tear_down` — it is noise. One declared in `set_up_before_script` lives for the whole +file; `bashunit::unmock` on it only suspends it for the current test, because each test +runs in its own subshell. + ## Data providers Each line of the provider becomes one run, split on whitespace into `$1`, `$2`, …: diff --git a/docs/test-doubles.md b/docs/test-doubles.md index e985ccaf..206a3fb2 100644 --- a/docs/test-doubles.md +++ b/docs/test-doubles.md @@ -126,6 +126,35 @@ test_example() { ``` ::: +## bashunit::unmock +> `bashunit::unmock "function"` + +Removes a double: it drops the function override — restoring the real command — and deletes the state files a spy recorded its calls in. It is a no-op for a name that was never mocked or spied. + +::: code-group +```bash [Example] +function test_example() { + bashunit::mock ls echo "mocked" + assert_same "mocked" "$(ls)" + + bashunit::unmock ls + + assert_not_same "mocked" "$(ls)" +} +``` +::: + +### Lifecycle of a double + +You rarely need to call it, because doubles are cleaned up for you: + +- A double created **inside a test** is removed after that test. There is no need to unmock in `tear_down`, and nothing leaks into the next test. +- A double created in **`set_up_before_script`** stays in place for every test in the file — that is the point of declaring it there. + +Each test runs in its own subshell, so `bashunit::unmock` only affects the test that calls it. Calling it on a `set_up_before_script` double suspends that double for the current test; the following tests still see it. + +Reach for it when a single test needs the real command back after exercising the mocked path, or when it needs a spy's recorded calls to start over mid-test. Re-declaring a double does not require it: a second `bashunit::mock` or `bashunit::spy` on the same command replaces the first. + ## bashunit::spy > `bashunit::spy "function"` diff --git a/tests/functional/doubles_lifecycle_test.sh b/tests/functional/doubles_lifecycle_test.sh new file mode 100644 index 00000000..d35a1891 --- /dev/null +++ b/tests/functional/doubles_lifecycle_test.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Pins the lifecycle documented in docs/test-doubles.md: doubles declared in +# set_up_before_script live for the whole file, doubles declared inside a test +# die with it, and bashunit::unmock reaches no further than the current test. + +function set_up_before_script() { + bashunit::mock lifecycle_shared_double echo "from before script" +} + +function test_a_before_script_double_is_visible_in_a_test() { + assert_same "from before script" "$(lifecycle_shared_double)" +} + +function test_b_unmock_only_affects_the_current_test() { + bashunit::unmock lifecycle_shared_double + + assert_empty "$(lifecycle_shared_double 2>/dev/null || true)" +} + +function test_c_before_script_double_survives_the_unmock_of_another_test() { + assert_same "from before script" "$(lifecycle_shared_double)" +} + +function test_d_a_double_declared_in_a_test_does_not_leak() { + bashunit::mock lifecycle_local_double echo "local" + + assert_same "local" "$(lifecycle_local_double)" +} + +function test_e_the_previous_test_double_is_gone() { + assert_empty "$(lifecycle_local_double 2>/dev/null || true)" +} + +function test_unmock_restores_the_real_command() { + bashunit::mock ls echo "mocked" + assert_same "mocked" "$(ls)" + + bashunit::unmock ls + + assert_not_same "mocked" "$(ls)" +} + +function test_unmock_forgets_the_calls_recorded_so_far() { + bashunit::spy lifecycle_spy + lifecycle_spy once + bashunit::unmock lifecycle_spy + + bashunit::spy lifecycle_spy + + assert_empty "$(assert_not_called lifecycle_spy 2>&1)" +} + +function test_unmock_of_an_unknown_name_is_a_no_op() { + bashunit::unmock lifecycle_never_mocked + + assert_successful_code "$?" +}