Provides testing best practices for writing reliable, maintainable test suites.
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:
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.
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.
// 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);
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) } |
/\ ~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.
mock.assert_called_once() as the
only assertion is a fraud smell — it survives any refactor and proves nothing.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>.Assert the outcome, so the test survives any refactor of how the outcome is produced and still fails when the outcome is wrong.
# 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:
#[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:
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)
}
}
}
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.
// 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.
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.
Non-obvious traps, verified against the stack before assuming otherwise.
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.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.@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).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.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.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.Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer