1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
# Bash Testing Rules
Applies to: `**/*.bats`
Implements the core principles from `testing.md`. All rules there apply here —
this file covers shell-specific patterns.
## Framework: bats-core
Use [bats-core](https://bats-core.readthedocs.io/) for shell tests. A test file
is `<thing>.bats`; each test is a `@test "description" { ... }` block; a non-zero
exit inside the block fails the test. Run a file with `bats path/to/file.bats`,
or a tree with `bats -r tests/`.
Drive the script under test with `run`: it captures `$status` (exit code),
`$output` (combined stdout+stderr), and `$lines[]` (output split by line)
without the failure aborting the test. Assert on those.
```bash
@test "greet: prints the name passed in" {
run bash "$SCRIPT" --name Ada
[ "$status" -eq 0 ]
[[ "$output" == *"Hello, Ada"* ]]
}
```
## Test the Real Script, Through Its Interface
Run the actual script file — never copy its logic into the test. Invoke it the
way a caller does (`run bash "$SCRIPT" <args>`, or `run "$SCRIPT"` when it's
executable) and assert on exit status and output. A test that re-implements the
script's logic passes even when the script breaks.
For a script that sources a library of functions, source the library in `setup`
and call the functions directly — that's the unit level; the `run` invocation is
the integration level.
## Normal, Boundary, Error — the Three Categories
Cover all three from `testing.md` per script:
- Normal: the expected arguments and inputs produce the expected output and a
zero exit.
- Boundary: empty argument, missing optional flag, single-item vs many,
whitespace and unicode in inputs, a path with a space.
- Error: missing required argument, nonexistent input file, a dependency
absent. Assert the exit code and that the error names the problem — not the
exact wording (`testing.md`'s error-behavior rule).
## Isolation and Determinism
- `setup()` makes a fresh `mktemp -d` per test; `teardown()` removes it. No test
leans on another's leftovers, and tests pass in any order.
- Mock an external command by putting a stub earlier on `PATH`: write a small
script named like the command into a temp dir, `chmod +x`, and prepend that
dir to `PATH` for the `run`. This is how you simulate a tool being absent,
returning an error, or emitting canned output — without touching the network
or the real tool.
- Never hardcode dates; generate them relative to `date` (see the
`task-review-staleness.bats` pattern in this repo for relative-date fixtures).
- Mock at the boundary (network, the external CLI, the clock). Don't mock the
script's own functions — those are the work.
## What Not to Do
- Don't assert exact error-message prose; assert the exit code plus a value the
message must contain.
- Don't share mutable state between tests through a fixed temp path.
- Don't test that `shellcheck` or `bats` themselves work — trust the tools.
- Don't skip the error cases because the happy path passes; the error paths are
where shell scripts actually break.
|