Skip to content

unit-test Skill Design Rationale

unit-test is a defect-first unit-testing framework for Go repositories. Its core idea is: the goal of high-quality unit tests is to first classify target risk, then design high-signal cases around explicit defect hypotheses, and finally explain why the suite deserves to exist through Killer Cases, boundary checklists, tiered scorecards, and real -race / coverage evidence. That is why the skill turns the Go Version Gate, Execution Modes, Defect-First Workflow, High-Signal Test Budget, Boundary Checklist, Auto Scorecard, Property-Based Testing, and Reporting Integrity into one fixed workflow.

1. Definition

unit-test is used for:

  • adding, strengthening, and fixing unit tests for Go code,
  • prioritizing real defects in boundaries, mappings, concurrency, and context propagation,
  • organizing tests into maintainable table-driven + t.Run structures,
  • selecting Light / Standard / Strict mode based on target risk,
  • and delivering tests together with race, coverage, scorecard, and residual-risk evidence.

Its output is not just test code. It also includes:

  • execution mode and rationale,
  • targets tested and case counts,
  • Go version and version-dependent adaptations,
  • boundary checklist,
  • coverage / race results,
  • scorecard and final PASS / FAIL.

Failure Hypothesis List, detailed Killer Case reporting, and the JSON summary mainly belong to Standard / Strict mode outputs; Light mode intentionally reduces output to a lighter boundary check and scorecard package.

From a design perspective, it is closer to a unit-test governance framework than to a prompt that merely adds a few _test.go files.

2. Background and Problems

The skill is not solving "models do not know how to write Go tests." It is solving the fact that default unit-test generation often drifts into low-signal patterns:

  • many tests exist, but the assertions are weak,
  • coverage looks strong, but the suite still misses real bugs,
  • lots of cases are present, but there is no visible testing methodology.

Without an explicit framework, the most common failures cluster into eight categories:

Problem Typical consequence
Coverage is chased without defect thinking many paths appear tested, but important bugs still escape
Target risk is not classified simple functions get over-engineered while risky concurrent code gets only shallow happy paths
No Killer Case exists maintainers cannot tell which assertions are the real regression barriers
Boundary coverage is not systematic nil, empty, singleton, last-element, and context-cancel cases are easily missed
Assertions are mutation-weak tests only check err == nil or not nil, so field-level regressions still pass
Test organization is scattered one target accumulates many separate TestXxx functions with poor reuse and high edit cost
Concurrency tests are nondeterministic time.Sleep, no -race, and leaked shared state make tests flaky
Output is not auditable the team cannot see why a mode was chosen, why the suite is considered sufficient, or what remains untested

The design logic of unit-test is to answer "how risky is this code, what is most likely to fail, which assertions form the regression barrier, what coverage gate is reasonable, and what level of test process should apply?" before deciding how many tests to write or how to organize them.

3. Comparison with Common Alternatives

It helps to compare it with a few common alternatives:

Dimension unit-test skill Asking a model to "write unit tests" Treating unit tests as a coverage-filling task
Defect-hypothesis-driven design Strong Weak Weak
Mode selection (Light / Standard / Strict) Strong Weak Weak
Killer Case discipline Strong Weak Weak
Boundary-checklist system Strong Medium Weak
Concurrency / -race awareness Strong Medium Weak
Test-organization consistency Strong Medium Weak
Quality scoring and auditability Strong Weak Weak
Coverage philosophy high-signal first, anti-bloat often ad hoc often metric-driven

Its value is not only that there are "more tests." Its value is that unit tests become an explainable, auditable, and maintainable defect-defense system instead of a pile of disconnected cases.

4. Core Design Rationale

4.1 Mode Selection Comes Before Case Design

In workflow step 0, unit-test requires choosing Light / Standard / Strict. That matters because it explicitly rejects a common mistake: applying one heavy test process to every target.

It routes based on factors such as:

  • target count,
  • presence of concurrency,
  • dependency complexity,
  • branch complexity,
  • security sensitivity,
  • context / deadline logic,
  • collection transforms or property-based triggers.

This design matters because:

  • simple pure functions can stay in Light and avoid unnecessary overhead,
  • ordinary business logic defaults to Standard,
  • concurrent, security-sensitive, or higher-risk targets are promoted to Strict.

What this solves is mismatch between test-process weight and code risk. Without it, the usual distortion is that trivial logic gets wrapped in oversized methodology while dangerous code receives only a handful of shallow cases.

4.2 The Defect-First Workflow Is Central

The most important design choice in unit-test is not table-driven structure and not coverage. It is the Defect-First Workflow. In Standard / Strict modes, the skill requires a Failure Hypothesis List before writing tests, covering at least:

  • loop / index risks,
  • collection-transform risks,
  • branching risks,
  • concurrency risks,
  • context / time risks.

This is critical because the highest-value part of unit testing is not "which parameter combinations exist," but "how is this code most likely to fail." For lower-risk targets, Light mode intentionally skips this heavier methodology layer so that simple unit tests do not turn into oversized process.

The evaluation proves this directly: with-skill and without-skill were almost identical in core functional-path coverage, and the largest gap came from Failure Hypothesis Lists, Killer Cases, and Boundary Checklists. In other words, the skill's main increment is not "testing more." It is "explaining much more systematically why these tests exist."

4.3 Killer Cases Are a Hard Constraint

In Standard + Strict modes, the skill requires at least one Killer Case per test target, and each Killer Case must contain four parts:

  1. defect hypothesis,
  2. fault injection or boundary setup,
  3. critical assertion,
  4. removal-risk statement.

This is one of the skill's most distinctive design choices. The difference between a normal edge case and a Killer Case is that a Killer Case must point to a named defect and explain "if this assertion is removed, which known bug can escape."

This solves a very practical maintenance problem: test files are often simplified, refactored, or partially deleted over time. Without the removal-risk layer, later maintainers cannot easily tell which assertions are decorative and which are the actual regression barrier. The evaluation showed that without-skill often covered the same paths, but lacked this explanatory layer, leaving the regression-defense boundary less explicit.

4.4 The Boundary Checklist Is Explicit vs. Implicit

unit-test turns boundary review into:

  • a 5-item checklist for Light,
  • a 12-item checklist for Standard / Strict.

The checklist covers:

  • nil,
  • empty,
  • singleton,
  • size / last-element boundary,
  • min/max boundaries,
  • invalid format,
  • zero-value struct/default trap,
  • dependency error,
  • context cancellation,
  • concurrent / race behavior,
  • mapping completeness,
  • killer-case mapping.

This is highly practical because missed boundaries are usually not caused by ignorance. They are missed because they do not naturally surface in a stable order during test writing. Once checklist discipline is explicit, test quality no longer depends on immediate memory alone; it has a reviewable baseline.

The evaluation also showed that without-skill did not necessarily omit boundary cases entirely, but it did omit the explicit checklist. That means the team could not quickly see which boundaries were systematically covered and which were only touched incidentally.

4.5 The Coverage Gate Is Scoped and Rationale-Based

The skill does not reduce coverage policy to a simplistic "always >= 80%." It explicitly distinguishes:

  • logic-heavy packages: default >= 80%,
  • infra / IO-heavy packages: possibly lower, but only with explicit rationale.

It also insists that:

  • coverage must not be inflated with low-signal tests,
  • even when coverage may be lower, boundary discipline still remains,
  • multi-package situations should use -coverpkg=./... or per-package profiles for honest measurement.

This is mature design because it rejects two common extremes:

  • treating coverage as the whole truth,
  • abandoning coverage gates entirely because they are imperfect.

unit-test instead keeps coverage as one quality threshold while never letting it replace defect-first design.

4.6 Assertion Mutation Resistance Is Emphasized So Strongly

The skill repeatedly requires mutation-resistant assertions while adapting assertion style to project convention. It supports:

  • require / assert in testify-based projects,
  • cmp.Diff when go-cmp is the established convention,
  • t.Fatalf / t.Errorf in stdlib-only projects,
  • no existence-only checks like err == nil or not nil,
  • business-field assertions rather than object existence.

This solves the core weakness of low-signal tests. A test that only proves "an object came back" may still pass when fields are swapped, defaults are wrong, or the last mapped item is missing. What the skill truly enforces is not one assertion library, but sufficient assertion strength to express business correctness.

The first rule in bug-finding-techniques.md is Mutation-Resistant Assertions, and the evaluation repeatedly supports the same point: the skill cares about which exact field must fail when behavior regresses, not just whether execution reached the path.

4.7 Test Structure Rules Are So Concrete

The skill requires:

  • top-level naming adapted to target type,
  • t.Run groups that map 1-to-1 to test targets,
  • table-driven cases,
  • defect-oriented readable case names,
  • preferring t.Parallel() when safe, but only when truly isolated.

This is not just style. It is about maintenance cost. In the evaluation, without-skill often produced many separate TestXxx functions. Functionally that was sometimes fine, but it makes incremental additions more expensive and repeats setup more often. With-skill consistently used table-driven + t.Run, making future case additions cheaper.

4.8 Concurrency Testing Is Designed Around Determinism

unit-test explicitly requires:

  • running go test -race,
  • avoiding time.Sleep for synchronization,
  • using channel barriers, WaitGroups, and channel sequencing for deterministic control,
  • avoiding unsafe t.Parallel() combinations.

This is important because the biggest risk in concurrent unit tests is not that they "will not run." It is that they fail only sometimes, or pass locally and fail in CI. The patterns in references/concurrency-testing.md effectively turn concurrency testing from "guessing scheduling with time" into "controlling scheduling with synchronization primitives." That is why -race is treated as a hard requirement rather than an optional enhancement.

4.9 Property-Based Testing Is Supported but Not Allowed to Replace Table-Driven Tests

The skill takes a restrained approach to property-based testing:

  • not applicable in Light,
  • optional recommendation in Standard,
  • required recommendation or implementation in Strict when the pattern fits.

It only recommends this path for patterns such as roundtrip, idempotency, preservation, commutativity, parse validity, and monotonicity, and it explicitly says:

  • property-based tests verify invariants,
  • table-driven tests verify exact boundaries and concrete outputs,
  • Killer Cases are still not replaced by property-based tests.

This is mature design because it avoids another testing distortion: seeing one invariant and trying to replace hand-written boundary tests with randomized checks. The skill's position is clear: property-based testing adds breadth; it does not replace defect-driven or boundary-driven tests.

4.10 Generated Code Exclusion Is Necessary

unit-test explicitly excludes:

  • *.pb.go,
  • *_gen.go,
  • wire_gen.go,
  • mock_*.go,
  • *_mock.go,
  • files marked with Code generated ... DO NOT EDIT.

This is necessary because generated code is usually better validated by its generator's own guarantees or by testing the higher-level behavior around it. Without this exclusion, a model can easily spend effort where coverage is easy to raise but long-term value is low. The important point is not that generated code should never be tested, but that the skill treats it as outside the default high-value scope.

4.11 Auto Scorecard and Reporting Integrity Exist Together

The skill's output is not allowed to stop at "tests added." It must report at least:

  • mode,
  • version adaptation,
  • boundary checklist,
  • coverage / race results,
  • a 13-item or 7-item scorecard,
  • final PASS / FAIL.

In Standard / Strict modes, it must additionally include:

  • hypothesis / killer-case mapping,
  • fuller methodology output,
  • JSON summary.

At the same time, Reporting Integrity says the model must not claim -race or coverage results unless they were actually run, and must provide exact commands when execution is not possible.

The value of this pairing is that unit tests become an auditable delivery artifact rather than only a code change. The team gets not only test files, but also answers to:

  • why this mode was selected,
  • which defects are covered, especially in Standard / Strict outputs,
  • which killer cases are critical in Standard / Strict,
  • whether coverage / race requirements were really met,
  • what residual risks remain.

The evaluation's clearest advantage also sits here: methodology output and audit traceability were the main differentiators.

4.12 Trigger Design Is Part of the Skill's Architecture

Unlike many skills, unit-test treats trigger accuracy as a first-class concern in evaluation, and the result was 20/20. That was not incidental; it came from deliberate Description design:

  • strong trigger keyword coverage,
  • explicit exclusions for benchmark / fuzz / integration / E2E / load / mock tasks,
  • strong imperative trigger wording,
  • and strong "cannot be replaced from memory" signals.

This belongs in the rationale because it shows that unit-test is not merely adding more rules. It is also solving "when should this skill activate, and when should it yield to another testing skill?"

4.13 Two Claims About a Killer Case, Two Experiments

A killer case makes two claims that read almost identically and are established by different experiments:

Claim Experiment Conclusion
This case catches defect D inject D; re-run test FAILS ⇒ holds
Assertion A is what catches D inject D and delete only A; re-run test PASSES ⇒ holds; test still FAILS ⇒ refuted

The skill originally mandated the second claim — every killer case had to state "if this assertion is removed, the known bug can escape detection" — while its workflow verified neither. Two separate defects followed from that.

The claim was unfalsifiable as written. Nothing in a green test run distinguishes an assertion that would catch the defect from one that would not, so the sentence was mandatory to write and impossible to check. It was reliably satisfied by assertion.

And the mandate was unsound. Even run correctly, injecting the defect cannot establish it. The repository's own exemplar proved this: with the dropped-tail mutation applied, deleting its length assertion left the test still failing, caught by the last-element identity assertion. The exemplar's mandated sentence was false about its own test — and test_removing_an_assertion_can_still_leave_the_mutation_caught now pins that by execution.

So the two claims are separated. The kill check is mandatory and reported as Kill: Verified (defect injected, failure observed and quoted) or Kill: Unverified (+ reason). An assertion-necessity claim is optional and permitted only after its own removal check — and the reference warns that the negative result is the common one, because a good case asserts cardinality and identity and most single defects trip both.

The underlying insight is that necessity is a property of an (assertion, defect) pair, not of an assertion. The identity assertion is indispensable against a reordering defect that preserves length; the length assertion is indispensable against a duplication defect that preserves identities; neither is indispensable against a dropped tail. Keep both — just do not claim either is load-bearing without the experiment that shows it.

Scorecard #11 scores the kill status, not the outcome: an honest Unverified still PASSES, while an unlabelled kill claim FAILS. Punishing an environment that cannot run Go would only teach the model to assert Verified falsely — the failure mode being closed, not a stricter version of it.

4.14 A Check on Form Cannot Certify a Claim About Content

Three separate defects in this skill's grader were the same mistake at different depths:

The check looked at It certified Counterexample that passed
a ```json fence exists "the summary is machine-readable" a fence containing NOT JSON
the document parses and has the fields "the summary is coherent" line_pct: 20, met: false, 13/13 PASS
the report is coherent "the report describes this test suite" "5 cases, H1+H2 covered" over a single 3-element input

Each fix moved the boundary one step from form toward content, and each gap was found by an experiment rather than by re-reading the rules — which is itself the argument for the skill's Reporting Integrity clause.

Two design rules came out of it. Validate types before consistency, and never guard a consistency rule with a type test — an isinstance guard turns a wrong type into a skipped check, so "critical_pass": "three" graded clean. Link the pillars that a verdict depends on: the coverage gate is Critical scorecard item 13, so a measured coverage miss must appear as a failed Critical item and an overall FAIL. Without that link every field can be locally consistent while the verdict is impossible.

The content-level check that closed the third row is deliberately narrow: run the emitted test with go test -v and compare the claimed case count against the cases the toolchain executed. That is not semantic analysis — it compares two things that already had to agree, which is usually where an over-claiming report breaks first.

4.15 Discovered, Ran, Verified — Three Different Things

A test case can be in three states that a naive check collapses into one:

State Evidence What it supports
Discovered the case exists and is named nothing
Ran go test -v printed a line for it nothing on its own — --- SKIP prints a line
Verified it reached PASS or FAIL its assertions executed

The skill's own harness collapsed ran into verified: it collected PASS, FAIL and SKIP alike, so changing two subtests to t.Skip() — names unchanged, count unchanged — left the grader reporting their hypothesis covered. Nothing had asserted anything.

The same confusion is available to the skill's user, which is why the rule now lives in Reporting Integrity rather than only in the grader: a boundary item marked Covered by a skipped case is the identical error, and a Kill: Verified label on a skipped killer case is worse — it asserts an executed result for a case that did not run.

The general form: a status is data, and dropping it converts a negative result into a positive one. Any check that reduces a per-item result set to a set of names has done this, whether the items are test cases, migrated rows, or linted files.

4.16 A Hypothesis Is Only Checkable If You Can Name the Mutation

The Failure Hypothesis List asks what could go wrong. Round 6 showed that this is not enough on its own: a hypothesis can be stated in prose, mapped to a case, matched by name — and still describe a behaviour that nothing asserts.

The exemplar's H2 promised "a nil/empty slice yields an empty but non-nil result". Its case supplied the input and asserted length and elements. len(nil) == 0, so changing the implementation to var out []string — which returns null where the contract promises [] — satisfied every assertion. The hypothesis had an input scenario and no verification.

The fix is a rule with teeth: for each hypothesis, name the change to the implementation that would violate it.

  • If you cannot name one, the hypothesis is not checkable as written. Rewrite it until you can — that rewriting is where a vague hypothesis becomes a testable one.
  • The case must carry an assertion that the named change would break. Supplying the input is not verification.

In the harness this became structural: every hypothesis owns a mutation, and the emitted test must kill each. Before that, the fixture kept three parallel lists — the wording, the required case, the mutation — and H2 appeared in two of them. Two lists that must agree drift; three is worse. Collapsing them into one entry per hypothesis makes the omission that hid H2's gap impossible to express.

A second lesson is about where the answer comes from. Whether non-nil is part of the contract is a question about the contract, not the implementation: make([]T, 0, n) happening to return non-nil today is not a promise. If the contract says it, assert it; if it does not, narrow the hypothesis. An exemplar that asserts more than the contract promises invents a requirement for the code it tests — which is the mirror image of the defect being fixed.

That mirror image needed its own guard, and a mutation run proved it: deleting the non-nil sentence from the source's doc comment left everything passing. So each hypothesis now carries two links pointing in opposite directions — a mutation (kill it, and the hypothesis is verified) and contract evidence in the source (match it, and the hypothesis is legitimate). A hypothesis with only the first can be a fabricated requirement; one with only the second is an unverified promise. Both are needed, and neither substitutes for the other.

5. Problems This Design Solves

Combining the current SKILL.md, key references, and the evaluation report, the skill solves the following problems:

Problem type Corresponding design Practical effect
Unit tests chase coverage only Defect-First Workflow Tests are shaped more like bug-finding than path-counting
High-risk and low-risk code use the same process Execution Modes Test-process weight matches code risk
Critical assertions get deleted during refactors Killer Case + Removal Risk Regression barriers become explicit
Boundary cases are missed Boundary Checklist Coverage becomes more systematic
Assertions are low-signal Mutation-Resistant Assertions Field, mapping, and state regressions are easier to catch
Concurrency tests are flaky -race + deterministic concurrency patterns Tests become more reliable
Test organization is scattered table-driven + t.Run + target adaptation Maintenance cost goes down
Test completeness is not auditable Scorecard + Reporting Integrity Teams can judge deliverability more clearly
A killer case's central claim is asserted, never checked Mandatory Kill: status (§4.13) The kill is either executed and quoted, or explicitly a hypothesis
"Kills the mutation" is read as "this assertion is indispensable" Two claims, two experiments (§4.13) The stronger claim is optional and requires its own removal check
A format check is trusted as a content check Types before consistency; linked pillars; report-vs-run comparison (§4.14) A coherent-looking report that contradicts its own code is rejected
A skipped case is read as coverage Discovered / ran / verified kept distinct (§4.15) --- SKIP no longer marks an item Covered or a kill Verified
A hypothesis states a behaviour nothing asserts Name the mutation per hypothesis (§4.16) An unmutatable hypothesis is rewritten, not shipped
A coverage number is read without its measurement scope Merged-profile rule + reason-based exclusions (§4.14) Covered code is not discarded from the gate, and a real gap cannot buy an exemption

6. Key Highlights

6.1 It Turns Unit Testing from a Coverage Exercise into a Defect-Discovery Process

This is the skill's most important strength and the main source of differentiation in evaluation.

6.2 Light / Standard / Strict Makes Test Intensity Match Risk

Not every code path deserves the same process overhead, and this skill makes that judgment explicit.

6.3 Killer Cases Are a Highly Distinctive Design Feature

For every Standard / Strict target, at least one case must clearly say "if this assertion disappears, this bug can escape."

6.4 Boundary Checklists and Scorecards Make Test Quality Auditable

The team no longer has to rely on intuition about whether the suite is "probably enough."

6.5 It Is Especially Strong on Concurrent and Time-Sensitive Go Code

Channel barriers, error fan-in, panic recovery, -race, and t.Parallel() safety rules show specialized design around Go's high-risk testing areas.

6.6 Its Real Increment Is Methodology and Auditability, Not Raw Path Count

The evaluation already shows that with-skill and without-skill were similar in core functional-path coverage. The real gap came from Failure Hypothesis Lists, Killer Cases, Boundary Checklists, test organization, and scorecards. That means the core value of unit-test is testing governance, not simply "generating more test cases."

7. When to Use It — and When Not To

Scenario Suitable Reason
Adding unit tests for Go logic code or boundary behavior Very suitable This is the core use case
Concurrent, context-sensitive, or mapping-heavy code with real bug risk Very suitable Defect-First + -race are highly targeted here
Reviewing or restructuring existing _test.go quality Very suitable Scorecard and Killer Case rules are practical
Benchmarks Not suitable Should go to benchmarking / performance workflows
Fuzz tests Not suitable Should go to fuzzing workflows
Integration / E2E / load tests Not suitable All are outside unit-test scope
Mock generation Not suitable That is not test design itself

8. Conclusion

The real strength of unit-test is not that it can write Go tests faster. It is that it systematizes the parts of unit testing that most often become formalistic: choose the right mode based on risk, list defect hypotheses first, design a Killer Case for each target, and then constrain the final suite with boundary discipline, mutation-resistant assertions, deterministic concurrency control, real coverage / race evidence, and a tiered scorecard.

From a design perspective, the skill expresses a clear principle: the key to high-quality unit testing is not running more functions once, but being able to explain which bugs the suite is preventing, why certain assertions cannot be removed, which boundaries have been systematically covered, which risks still remain, and whether those conclusions are backed by real -race and coverage evidence. That is why it is especially well suited to Go logic testing, concurrency-sensitive code, and test-quality improvement work.

9. Document Maintenance

This document should be updated when:

  • the Hard Rules, Execution Modes, Defect-First Workflow, Coverage Gate, Auto Scorecard, Property-Based Testing, Reporting Integrity, or Output Expectations in skills/unit-test/SKILL.md change,
  • key patterns or examples in skills/unit-test/references/killer-case-patterns.md, bug-finding-techniques.md, concurrency-testing.md, or property-based-testing.md change,
  • key supporting results in evaluate/unit-test-skill-eval-report.md or evaluate/unit-test-skill-eval-report.zh-CN.md change.

Review quarterly; review immediately if the mode-selection rules, Killer Case discipline, scorecard structure, or trigger description of unit-test change substantially.

10. Further Reading

  • skills/unit-test/SKILL.md
  • skills/unit-test/references/killer-case-patterns.md
  • skills/unit-test/references/bug-finding-techniques.md
  • skills/unit-test/references/concurrency-testing.md
  • skills/unit-test/references/property-based-testing.md
  • skills/unit-test/scripts/tests/COVERAGE.md
  • evaluate/unit-test-skill-eval-report.md
  • evaluate/unit-test-skill-eval-report.zh-CN.md