| name | codegen-test |
| description | A skill for writing Go tests in a TDD workflow. Follow README/Issue requirements and existing test conventions, add a balanced set of table-driven cases across behavior/boundaries/branches, and produce maintainable tests without excessive mocking or coverage-chasing. |
Codegen Test Skill
Purpose
- Lock down the spec (expected behavior) based on README.md / Issue and run Red → Green → Refactor.
- Write idiomatic, readable, maintainable Go tests (table-driven, subtests, small helpers).
- Avoid focusing only on branch coverage; also add boundary, black-box (behavior), and regression perspectives.
- Do not make coverage the “goal.” Do not write tests that reduce maintainability.
- Avoid mocks as much as possible; implement in a more classicist style.
When to use
- When you want to write tests first for a feature, bug fix, or refactor.
- When existing behavior is unclear and you want to build consensus (freeze the spec) via tests.
- When you want to add regression tests to prevent recurrence.
Deliverables (expected output)
- Tests that transition from failing → passing (Red → Green is observable)
- Test code aligned with the project’s existing test style
- Commands executed (go test / race / optionally bench) and brief result notes
Execution steps (follow this order)
0) Safety measures before changes
- Tests become the spec (contract). Do not contradict README/Issue.
- If existing tests act as the de-facto spec, prioritize and do not break them.
- Start with the minimum necessary tests, then incrementally add perspectives as needed.
1) Understand the spec (README.md / Issue)
- From README.md / docs / comments / Issue, finalize acceptance criteria (expected behavior) as bullet points.
- Break them down (ideally usable as test case names):
- Representative happy-path cases
- Error cases (invalid input, broken preconditions, dependency failures)
- Boundary values (empty, 0, min/max, off-by-one)
- Concurrency (simultaneous execution, ordering, shared state)
- Compatibility (preserve past behavior, regression prevention)
2) Follow existing test conventions (highest priority)
- Do not introduce additional external modules.
- Search existing
_test.gofiles and match the following:- Package style (
package xvspackage x_test) - Assertion approach (stdlib
testingvs an existing assertion library already used) - Table-driven style (
tests/tt, field naming patterns) - Helper locations, naming, fixture patterns
- Test data layout (whether
testdata/exists)
- Package style (
This skill is not about imposing new rules; it prioritizes matching the existing conventions.
3) Test design principles
- Prefer black-box testing (behavior of the public API).
- Do not directly test implementation details (private functions/internal state).
- Exception: Only when “hard to observe from the outside” requirements matter (e.g., performance or race-safety), and only with minimal supplemental tests.
- Do not write trivial tests (e.g., “assert the type”).
4) Use table-driven tests as the default shape
- If multiple conditions exercise the same logic, use a table-driven test.
- Recommended structure:
tests := []struct{ name string; input ...; want ... }{ ... }for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) }
- Prefer
input/wantprefixes for field names. - Do not stuff functions or complex branching (e.g.,
shouldX,setupMocks func...) into the table.- If it becomes complex, split the table or split the test function.
5) Balance branch coverage (white-box) with behavior/boundaries (black-box)
- Branch coverage: ensure each if/switch branch is exercised at least once.
- Boundaries: add empty/min/max and just-before/just-after cases (off-by-one).
- Behavior: include “what must be guaranteed” in the test name so it’s understandable to readers.
6) Avoid mocks as much as possible (classicist)
- Principle: use something “close to real.”
- Examples: in-memory implementations,
bytes.Buffer,httptest,t.TempDir(),net.Pipe()
- Examples: in-memory implementations,
- Do not over-introduce interfaces (often harms design and maintainability).
- If external dependencies must be cut, swap them at the smallest boundary.
- If large mock setups are required, prefer splitting the unit under test into smaller components.
7) Concurrency tests (only when needed, carefully)
- Make race-prone areas verifiable with
-race. - If using
t.Parallel(), always rebind loop variables:- Do
tt := ttimmediately beforet.Runto avoid capture bugs.
- Do
- For concurrent cases, explicitly state what is being guaranteed (no races, ordering, idempotency, etc.).
8) Run and verify (required)
- Run package-level (or full) tests including race checks:
task go:test
- If performance is a key concern, add/run benchmarks (only when necessary):
task go:bench
Prohibited (do not do)
- Tests that are hard to read/brittle written only to raise coverage
- Complex branching inside table-driven tests or excessive flags (hurts maintainability)
- Tests tightly coupled to implementation details (not refactor-friendly)
- Unnecessary interfaces or heavy reliance on mocks
Final report about test code and approach (keep it short)
- Output as a Markdown report:
- Filename:
<issue_number>_<datetime>_test_report.md
- Filename:
- Include:
- Spec summary (acceptance criteria from README/Issue)
- Added test perspectives (branches/boundaries/behavior/regression/concurrency)
- Commands executed (test / race / bench)
- Reasons for tests intentionally not written (e.g., avoiding excessive mocks), if needed