aboutsummaryrefslogtreecommitdiff
path: root/languages/bash/claude/rules/bash-testing.md
blob: c904927df83bb1059e1977509f83923b4f765149 (plain)
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.