Testing · git:20260715.857b62e · 2026-07-15 · sha256 ca362aaad089ccaa
Testing git:20260715.857b62eA
Immutable. This exact content never changes and is served at /api/v1/blob/ca362aaad089ccaa.
---
name: Testing
description: >
How to WRITE real, maintainable tests in any stack — assertions that fail when the
code is broken, the test pyramid and AAA structure, behavior-over-implementation,
mocking discipline, fixtures, coverage that means something, and the ship gates
(test-count-never-decrease, no self-mocking, prove-RED-first). Language-generic with
concrete examples in TypeScript/JavaScript, Python, Rust, and Go. Use when asked to
"add tests", "write a test", "improve coverage", "fix a flaky test", "how do I test X",
when turning a hollow assertion into a real one, deciding what layer a test belongs in,
or when a change must not drop test quality. The construction counterpart to the
crucible-detective agent's fraud hunt.
when_to_use: >
add tests, write tests, improve coverage, fix a flaky test, mock vs real dependency,
unit vs integration vs e2e, turn a hollow assertion into a real one, set up fixtures or
test doubles, choose a coverage target, wire a CI test gate, pre-commit test gate,
test-count-never-decrease enforcement, "is this test real", test a function / module /
API / component in TS, JS, Python, Rust, or Go.
allowed-tools: Read, Grep, Glob, Bash
---
# Testing — Writing Tests That Catch Real Bugs
A test that stays green while the code is broken is **worse than no test** — it
manufactures false confidence. This skill is the *construction* companion to the
crucible-detective agent:
- **crucible-audit** skill — *detection*: the 8 fraud patterns (hollow assertions,
mock proliferation, dead tests, coverage-config lies) + the language matrix.
- **testing** (this skill) — *construction*: principles + how to write a test that
actually fails on a real bug, in whatever stack the project uses.
Guidance is language-generic; examples cover the stacks Forge users build in —
TypeScript/JavaScript, Python, Rust, Go. Match the project before you copy a snippet.
## The one rule that makes a test real
Every test must **fail when you break the thing it names.** Before you trust a new
test, prove it: change the production code to be wrong, re-run, confirm **RED**, then
revert. An assertion that survives a deliberately broken implementation is theater.
```js
// THEATER — passes even if computeDiscount() returns garbage
expect(result).toBeDefined();
expect(result.total).toBeTruthy();
// REAL — pins the actual contract; breaks the instant the math changes
expect(result.total).toBe(87.5);
expect(result.appliedRule).toBe('bulk-10pct');
expect(result.lineItems).toHaveLength(3);
```
### Hollow tells to avoid, by stack
crucible-audit hunts these — don't write them. An assertion that cannot fail on a
*wrong value* is not a test.
| Stack | Never assert only | Assert instead |
|-------|-------------------|----------------|
| vitest / jest (TS/JS) | `toBeDefined`, `toBeTruthy`, `not.toBeNull` | exact value, shape (`objectContaining`), length |
| node:test (JS) | `assert.ok(x)`, `assert(x !== null)` | `assert.strictEqual`, `assert.deepStrictEqual` |
| pytest (Python) | `assert x is not None`, `assert x`, `assert result` | `assert x == expected`, `assert x.status == "active"` |
| Rust | `assert!(x.is_some())`, `assert!(x.is_ok())` | `assert_eq!(x.unwrap(), expected)` |
| Go | `if got == nil { t.Fatal(...) }` alone | `if got != want { t.Errorf("got %v, want %v", got, want) }` |
## The test pyramid
```
/\ ~10% E2E drive the real binary / full UI flow (slow, high-value)
/--\ ~20% Integration real component seams together (real fs, DB, HTTP, state)
/----\ ~70% Unit one unit, isolated, deterministic, < 100ms
```
A suite that is 100% unit tests with mocked seams proves the units, never the system.
"Unit tests pass but nothing works" is caught at the integration and e2e layers
(crucible Pattern 5). A "unit" test that touches a real DB, network, or port is
misclassified — move it to `integration/` or mock the seam.
Layer-by-layer patterns per stack: [reference/patterns-by-layer.md](reference/patterns-by-layer.md).
## Five non-negotiable principles
1. **Assert behavior, not implementation.** Check the returned value / observable
effect, not that some internal mock was called. `mock.assert_called_once()` as the
*only* assertion is a fraud smell — it survives any refactor and proves nothing.
2. **Descriptive names as specs.** `register_with_duplicate_email_raises` — not
`test_user`, `test_1`, `it('works')`. A vague name is where a hollow test hides.
Shape: `<unit>_<scenario>_<expected>`.
3. **One reason to fail per test.** Group related asserts; split unrelated ones.
4. **Isolated & repeatable.** No shared mutable state, no order dependence, no
sleep-to-synchronize. Each test creates and tears down its own fixtures.
5. **Right layer for the seam.** Mock external collaborators in unit tests; use the
*real* seam in integration tests.
## Worked example — behavior vs implementation
Assert the **outcome**, so the test survives any refactor of *how* the outcome is
produced and still fails when the outcome is *wrong*.
```python
# GOOD — asserts the OUTCOME; survives a rewrite of the hashing internals
def test_register_creates_active_user():
user = register_user("a@example.com", "SecurePass123!")
assert user.email == "a@example.com"
assert user.is_active is True
# BAD — asserts HOW; breaks on refactor, proves nothing about correctness
def test_register_calls_bcrypt():
register_user("a@example.com", "SecurePass123!")
bcrypt.hash.assert_called_once() # implementation detail, not behavior
```
The same shape in Rust — act on the real unit, assert the real value:
```rust
#[test]
fn claim_task_moves_status_to_in_progress() {
let mut board = TaskBoard::new_in_temp(); // Arrange
let id = board.add_task("implement auth");
let claimed = board.claim_task(&id, "worker").unwrap(); // Act
assert_eq!(claimed.status, TaskStatus::InProgress); // Assert value
assert_eq!(claimed.assignee.as_deref(), Some("worker"));
}
```
And in Go — table-driven, comparing against an expected value:
```go
func TestSnakeCase(t *testing.T) {
cases := []struct{ in, want string }{
{"HelloWorld", "hello_world"},
{"testCamelCase", "test_camel_case"},
}
for _, c := range cases {
if got := SnakeCase(c.in); got != c.want {
t.Errorf("SnakeCase(%q) = %q, want %q", c.in, got, c.want)
}
}
}
```
## Mocking discipline
Mock the *boundary*, never the *thing under test.* Mock the network, the clock, a
paid API, the filesystem when it's not what you're verifying — but if a test mocks the
very function whose behavior it claims to prove, it proves nothing (crucible Pattern 3:
mock proliferation). **Prefer a real fixture** — a temp directory, an in-memory DB, a
real object — over a stub that returns the exact answer you're asserting.
```js
// WRONG — the mock IS the assertion; the real code never runs (tautology)
vi.mock('../discount.js', () => ({ computeDiscount: () => ({ total: 87.5 }) }));
expect(computeDiscount(cart).total).toBe(87.5);
// RIGHT — run the real function against a controlled input
const result = computeDiscount(loadFixtureCart('bulk-order'));
expect(result.total).toBe(87.5);
```
Test-double taxonomy, builders, and fixture patterns:
[reference/doubles-fixtures-coverage.md](reference/doubles-fixtures-coverage.md).
## Coverage — a floor and a smell detector, not a goal
Coverage is a **map of what RAN, not proof it was verified.** A line executed by a
test with a hollow assertion counts as "covered" while catching nothing. 85% coverage
built on `toBeDefined()` proves nothing. Never quote a coverage % without the
assertion-quality read beside it.
Rough targets (guideline, not a finish line):
| Module type | Target |
|-------------|--------|
| Core / domain logic | ≥ 90% |
| Application services / API handlers | ≥ 85% |
| Infrastructure / adapters / CLI | ≥ 80% |
| **Overall** | **≥ 85%** |
Security-, auth-, payment-, and financial-calculation code should approach 100% **and
be genuinely asserted.** Legitimately-low-coverage code (rare error branches, defensive
asserts, platform-compat, pure formatting) is justified, not chased.
## Ship gates (enforced by crucible-detective + code-insurance-check)
1. **Test count never decreases.** A PR that removes tests without a stated, reviewed
reason fails the gate. Deleting a failing test to go green is fraud. (Caveat: the
count is a floor on *quantity*, never *proof* — a hollow test satisfies it while
asserting nothing.)
2. **No self-mocking** of the unit under test (see Mocking discipline).
3. **New behavior ⇒ new test.** Every feature/fix that changes runtime behavior lands
with a test that would have caught the old bug.
4. **Prove RED first** for any test guarding a real bug — break the code, watch it
fail, revert.
## Gotchas
Non-obvious traps, verified against the stack before assuming otherwise.
- **A module that connects/starts a server at import time hangs the test run.** If the
entry module calls `server.connect()` / `app.listen()` / opens stdio at top level,
merely *importing* it in a test blocks forever. Guard the side effect behind an env
flag (`if (!process.env.TEST_MODE) server.connect()`) or an `if __name__ ==
"__main__"` / `func main()` boundary, and import only the pure functions in tests.
- **Coverage % lies under an omit list.** `coveragePathIgnorePatterns` (jest/vitest),
`omit` in `.coveragerc`/`pyproject.toml`, `collectCoverageFrom` exclusions, or Go
build tags can inflate the number by hiding untested code. Read the omit list before
trusting any coverage badge — the gap between claimed and real coverage is exactly
what got excluded.
- **Marked-but-unrun tests count as "passing."** An `@e2e` / GPU / hardware-gated test
that CI filters out and never executes still shows green and inflates the count.
Verify the CI job actually *runs* the mark, not just collects it (crucible Pattern 4).
- **Parallel tests sharing a resource flake intermittently.** Tests that contend on a
real file lock, a fixed port, or a shared DB row collide only sometimes under
parallel runners → non-deterministic RED. Reproduce/diagnose serially (`cargo test --
--test-threads=1`, `pytest -p no:xdist`, `go test -p 1`), but keep the parallel run
as CI truth once isolated — a flaky test is usually shared state, not a logic bug.
- **Detection greps are language-shaped.** A grep for `assert .* is not None` finds
Python hollow tells and returns 0 on a JS/Rust/Go repo → false CLEAN. Translate to
the stack's tells (hollow-tell table above) before auditing.
- **Never `git add -A` for a test commit.** Stage the specific test files; a stray
build artifact, `.env`, or editor dir swept in breaks release hygiene and can leak
secrets.
## Additional resources
- Unit / integration / e2e patterns per stack (AAA, naming, parametrization, real-DB
integration, Playwright e2e) — [reference/patterns-by-layer.md](reference/patterns-by-layer.md)
- Test doubles, fixtures, builders, coverage measurement, async/concurrency, perf
budgets — [reference/doubles-fixtures-coverage.md](reference/doubles-fixtures-coverage.md)
- Per-language run commands, CI gate shape, test organization, DO/DON'T checklist —
[reference/ci-and-commands.md](reference/ci-and-commands.md)
- Fraud-detection methodology + the 8 patterns + mutation testing → the
**crucible-audit** skill.