test-generation
Purpose
Turn existing code into tests that would actually catch it breaking. The input is a function,
class, module, or bug report; the output is test files in the project's own framework, in the
right source set, that run and pass — plus a short statement of what is still untested.
The value is in case selection, not in typing. A test that only walks the happy path documents
the code; it does not defend it. Most real defects live at the boundaries and in the error paths,
so that is where the cases go.
When to use
- "Write tests for
<file/function/class>" — new or existing code
- A bug was just fixed and needs a regression test that fails without the fix
- A module has no tests and needs a suite scaffolded
- Coverage is low and the question is what to test next, not how much
Not this skill:
- Whole-project audit that happens to mention missing tests →
code-analyzer
- A test or app is failing and you need the root cause →
error-debugging
- Setting up a test framework that does not exist in the project yet → do that first (see the stack reference), then come back
Route to a more specific skill first
Several stacks already have dedicated skills. When one applies, invoke it instead of
reimplementing its guidance here; use this skill for the surrounding decisions (which cases,
which source set, coverage) and only where no dedicated skill exists.
| Situation |
Skill to invoke |
Dart unit tests with package:test |
dart-add-unit-test |
Dart mocks via mockito + build_runner |
dart-generate-test-mocks |
| Flutter widget tests |
flutter-add-widget-test |
Flutter integration_test flows |
flutter-add-integration-test |
| Compose / CMP UI tests, semantics, screenshot tests |
compose-ui-testing-patterns |
Swift, Rust, C++, and non-UI Kotlin/KMP have no dedicated skill — this one covers them directly.
Prerequisites
1. What stack is this, and what framework is already in use? Detect from build files, not from
the file extension alone — a .kt file in a KMP commonTest source set is tested very differently
from one in an Android unit test.
| Build file / signal |
Stack |
Reference |
build.gradle.kts with kotlin("multiplatform"), commonTest |
Kotlin Multiplatform |
kotlin-kmp.md |
build.gradle(.kts) with com.android.application, androidTest/ |
Kotlin/JVM, Android |
kotlin-kmp.md |
androidx.compose.* or compose-multiplatform dependencies |
Compose / CMP |
compose-ui-testing-patterns skill |
pubspec.yaml |
Flutter / Dart |
flutter-dart.md |
Package.swift, *.xcodeproj, *.xcworkspace |
Swift (iOS/macOS) |
swift-apple.md |
Cargo.toml |
Rust |
rust-cpp.md |
CMakeLists.txt, conanfile.txt, *.cpp |
C / C++ |
rust-cpp.md |
package.json, pyproject.toml, go.mod, Maven pom.xml |
JS/TS, Python, Go, Java |
other-languages.md |
Load exactly one stack reference — the one that matches. Loading several mixes idioms that do not
apply and wastes context.
2. Does a test suite already exist? Read two or three existing test files before writing
anything. The project's naming, assertion library, fixture style, and mocking approach are already
decided; matching them matters more than any convention in this skill.
# find the existing suite and its conventions
find . -type d \( -name test -o -name tests -o -name '*Test*' -o -name commonTest \) -not -path '*/build/*' -not -path '*/node_modules/*' | head -20
If there is no suite at all, say so — the first test in a project often needs a framework
dependency and a runner configuration, which is a larger change than the user may expect.
Workflow
Step 1: Read the code under test
Read the actual implementation, not just the signature. You are looking for the things a test can
assert on and the things that make it hard to test:
- Contract — inputs, outputs, thrown/returned errors, documented invariants
- Branches — every
if, when/switch, early return, loop boundary, and ?: fallback
- Dependencies — what has to be substituted (clock, network, filesystem, database, random)
- State — is the result a pure function of the inputs, or does it depend on prior calls?
- Concurrency — suspending functions, async/await, threads, actors, channels
Use symbol-aware tools (Serena) to read the enclosing symbol and its call sites rather than whole
files. Call sites show how the code is really used, which is where the realistic cases come from.
If the code is untestable as written (hard-wired singleton, hidden clock, constructor doing I/O),
say so and name the smallest change that would fix it. Do not silently refactor production code to
make a test possible — propose it, and let the user decide.
Step 2: Choose the cases
Derive cases from the contract and the branches, not from a template. For each unit under test,
work through these categories and keep the ones that are real for this code:
| Category |
Ask |
| Happy path |
The typical call with typical values — one per meaningful behavior, not per method |
| Boundaries |
Zero, one, many; first, last; min, max; off-by-one on every index and length |
| Empty / absent |
Empty string, empty collection, null/None/nil, absent optional, default argument |
| Invalid input |
Malformed data, out-of-range values, wrong type where the language permits it |
| Error paths |
Every thrown exception, Err, failed Future, and the recovery around it |
| State transitions |
Called twice, called out of order, called after close/dispose |
| Concurrency |
Cancellation, timeout, racing callers — only where the code is actually concurrent |
Two rules that decide most of the case list:
- One behavior per test. A test that asserts five things fails with one message and tells you
nothing about the other four.
- A case earns its place by being able to fail. If no plausible implementation change makes the
test go red, it is not testing anything — drop it.
For a bug-fix regression test, invert the order: write the test that reproduces the bug first,
confirm it fails against the unfixed code (or explain why that is not possible), then keep it.
Case-selection heuristics, naming, and fixture/mock/fake choice are in
test-design.md — load it when the cases are not obvious from the
contract.
Step 3: Place the file correctly
Wrong placement is the most common way a generated test never runs. The stack reference gives the
exact rules; the shape is always the same:
- Mirror the source path in the test source root, and name the file after the unit under test
- Pick the source set by what the test needs: shared/pure logic goes in the common or plain unit
test set; anything needing a device, a real framework, or a UI goes in the platform/instrumented
set
- Match the existing suite's package/module declaration
Step 4: Write the tests
Follow the conventions found in Step "Prerequisites 2" and the idioms in the stack reference:
- Arrange, act, assert — visibly separated, in that order
- Descriptive names — the name states the condition and the expected result, so a failure is
readable without opening the file
- Assert the behavior, not the implementation — asserting on internal calls freezes the design
and breaks on every refactor
- Substitute only what you must — a real object beats a fake, a fake beats a mock; mocking
types you own is usually a design smell
- Deterministic by construction — inject the clock, seed the random source, control the
dispatcher/scheduler; never
sleep to wait for async work
- Fixtures over duplication — but a little duplication in tests is cheaper than a fixture
hierarchy nobody can follow
Write the tests the project's existing suite would recognize as its own.
Step 5: Run them
A generated test that was never executed is a draft. Run the suite — narrowed to the new tests
first, then the full file — and fix what fails.
The stack reference has the exact command. Keep the output small: filter to failures rather than
pasting the whole run.
Then verify the tests are worth having: change the implementation in a way that should break a
test (mentally, or actually and revert) and confirm the right test would catch it. Report honestly
if a test passes against a deliberately broken implementation — that means it is asserting nothing.
Step 6: Report coverage and the gaps
Run the project's coverage tool if it is configured (Kover, flutter test --coverage, xccov,
cargo llvm-cov, gcov/llvm-cov, or the JS/Python equivalents — see the stack reference). If it
is not configured, do not add it unasked; report the gaps by reading the branches instead.
Report the gaps, not the percentage:
## Tests added
<file paths, and one line per behavior covered>
## Verification
<command run, and the real result>
## Still untested
<specific branches or paths, as `file:line` — with why, if there is a reason>
Coverage is a way of finding untested branches, not a target. Do not add tests whose only purpose
is to raise the number.
Operations
| Operation |
User intent |
Output |
unit (default) |
"write tests for this function/class" |
Isolated tests with dependencies substituted, run and green |
integration |
"test this end to end", "test the module together" |
Tests across real collaborators — DB, HTTP, filesystem — with setup/teardown |
ui |
"test this screen/widget/view" |
UI tests via the stack's UI harness; routes to the dedicated skill where one exists |
regression |
"write a test for the bug I just fixed" |
One focused test that fails without the fix |
coverage |
"what is not tested" |
Coverage run plus a ranked list of untested branches; tests only if asked |
Critical constraints
- Never assert on invented behavior. Read the implementation; if the intended behavior is
genuinely ambiguous, ask rather than encoding a guess as an assertion.
- Never write a test you have not run. Report failures rather than hiding them.
- Do not modify production code to make a test pass — that inverts the point. Propose the
change separately if the code is untestable.
- Do not rewrite or "improve" the existing test suite while adding tests to it.
- Do not delete or weaken a failing test you did not write. A red test is information.
- No sleeps, no wall-clock dependence, no network in unit tests. Flaky tests are worse than
missing ones because they train people to ignore red.
- Do not chase a coverage number. Assertion-free tests and getter tests inflate coverage and
defend nothing.
- Never put real credentials, tokens, or production data in fixtures. Test data is committed
and public.
References
- test-design.md — cross-cutting case selection, naming, fixtures vs fakes vs mocks, and how to read a coverage report
- kotlin-kmp.md —
kotlin.test and JUnit, source sets, runTest/TestDispatcher, Turbine, MockK, Kover
- flutter-dart.md — routing to the dedicated Dart/Flutter skills, plus goldens and coverage
- swift-apple.md — Swift Testing, XCTest, XCUITest, async tests,
xccov
- rust-cpp.md —
cargo test, doc-tests, proptest, criterion, llvm-cov; GoogleTest, Catch2, CTest
- other-languages.md — Jest/Vitest, pytest, Go
testing, plain JUnit
1---2name: test-generation3description: Write tests for existing code: unit tests, integration tests, UI tests, edge cases and error paths, plus coverage analysis to find what is still untested. Detects the project's test framework from its build files and follows that project's conventions — Kotlin/KMP (kotlin.test, coroutines-test, Turbine, MockK, Kover), Compose Multiplatform, Flutter/Dart, Swift (Swift Testing, XCTest, XCUITest), Rust (cargo test, proptest, criterion), C++ (GoogleTest, Catch2), and secondarily Jest/Vitest, pytest, Go and JUnit. Use when someone says 'write tests for this', 'add unit tests', 'test this function', 'add an integration test', 'scaffold a test suite', 'what is not covered', 'improve test coverage', 'write a regression test for this bug', or the Hungarian 'írj teszteket', 'teszteld le ezt a függvényt', 'mi nincs letesztelve'. Not for auditing a whole project for bugs and missing tests in general (that is code-analyzer), nor for diagnosing a crash or a failing test's root cause (that is error-debugging).4---56# test-generation78## Purpose910Turn existing code into tests that would actually catch it breaking. The input is a function,11class, module, or bug report; the output is test files in the project's own framework, in the12right source set, that run and pass — plus a short statement of what is still untested.1314The value is in case selection, not in typing. A test that only walks the happy path documents15the code; it does not defend it. Most real defects live at the boundaries and in the error paths,16so that is where the cases go.1718## When to use1920- "Write tests for `<file/function/class>`" — new or existing code21- A bug was just fixed and needs a regression test that fails without the fix22- A module has no tests and needs a suite scaffolded23- Coverage is low and the question is *what* to test next, not *how much*2425Not this skill:2627- **Whole-project audit that happens to mention missing tests** → `code-analyzer`28- **A test or app is failing and you need the root cause** → `error-debugging`29- **Setting up a test framework that does not exist in the project yet** → do that first (see the stack reference), then come back3031## Route to a more specific skill first3233Several stacks already have dedicated skills. When one applies, invoke it instead of34reimplementing its guidance here; use this skill for the surrounding decisions (which cases,35which source set, coverage) and only where no dedicated skill exists.3637| Situation | Skill to invoke |38|---|---|39| Dart unit tests with `package:test` | `dart-add-unit-test` |40| Dart mocks via `mockito` + `build_runner` | `dart-generate-test-mocks` |41| Flutter widget tests | `flutter-add-widget-test` |42| Flutter `integration_test` flows | `flutter-add-integration-test` |43| Compose / CMP UI tests, semantics, screenshot tests | `compose-ui-testing-patterns` |4445Swift, Rust, C++, and non-UI Kotlin/KMP have no dedicated skill — this one covers them directly.4647## Prerequisites4849**1. What stack is this, and what framework is already in use?** Detect from build files, not from50the file extension alone — a `.kt` file in a KMP `commonTest` source set is tested very differently51from one in an Android unit test.5253| Build file / signal | Stack | Reference |54|---|---|---|55| `build.gradle.kts` with `kotlin("multiplatform")`, `commonTest` | Kotlin Multiplatform | [kotlin-kmp.md](references/kotlin-kmp.md) |56| `build.gradle(.kts)` with `com.android.application`, `androidTest/` | Kotlin/JVM, Android | [kotlin-kmp.md](references/kotlin-kmp.md) |57| `androidx.compose.*` or `compose-multiplatform` dependencies | Compose / CMP | `compose-ui-testing-patterns` skill |58| `pubspec.yaml` | Flutter / Dart | [flutter-dart.md](references/flutter-dart.md) |59| `Package.swift`, `*.xcodeproj`, `*.xcworkspace` | Swift (iOS/macOS) | [swift-apple.md](references/swift-apple.md) |60| `Cargo.toml` | Rust | [rust-cpp.md](references/rust-cpp.md) |61| `CMakeLists.txt`, `conanfile.txt`, `*.cpp` | C / C++ | [rust-cpp.md](references/rust-cpp.md) |62| `package.json`, `pyproject.toml`, `go.mod`, Maven `pom.xml` | JS/TS, Python, Go, Java | [other-languages.md](references/other-languages.md) |6364Load exactly one stack reference — the one that matches. Loading several mixes idioms that do not65apply and wastes context.6667**2. Does a test suite already exist?** Read two or three existing test files before writing68anything. The project's naming, assertion library, fixture style, and mocking approach are already69decided; matching them matters more than any convention in this skill.7071```bash72# find the existing suite and its conventions73find . -type d \( -name test -o -name tests -o -name '*Test*' -o -name commonTest \) -not -path '*/build/*' -not -path '*/node_modules/*' | head -2074```7576If there is no suite at all, say so — the first test in a project often needs a framework77dependency and a runner configuration, which is a larger change than the user may expect.7879## Workflow8081### Step 1: Read the code under test8283Read the actual implementation, not just the signature. You are looking for the things a test can84assert on and the things that make it hard to test:8586- **Contract** — inputs, outputs, thrown/returned errors, documented invariants87- **Branches** — every `if`, `when`/`switch`, early return, loop boundary, and `?:` fallback88- **Dependencies** — what has to be substituted (clock, network, filesystem, database, random)89- **State** — is the result a pure function of the inputs, or does it depend on prior calls?90- **Concurrency** — suspending functions, async/await, threads, actors, channels9192Use symbol-aware tools (Serena) to read the enclosing symbol and its call sites rather than whole93files. Call sites show how the code is really used, which is where the realistic cases come from.9495If the code is untestable as written (hard-wired singleton, hidden clock, constructor doing I/O),96say so and name the smallest change that would fix it. Do not silently refactor production code to97make a test possible — propose it, and let the user decide.9899### Step 2: Choose the cases100101Derive cases from the contract and the branches, not from a template. For each unit under test,102work through these categories and keep the ones that are real for this code:103104| Category | Ask |105|---|---|106| Happy path | The typical call with typical values — one per meaningful behavior, not per method |107| Boundaries | Zero, one, many; first, last; min, max; off-by-one on every index and length |108| Empty / absent | Empty string, empty collection, null/`None`/`nil`, absent optional, default argument |109| Invalid input | Malformed data, out-of-range values, wrong type where the language permits it |110| Error paths | Every thrown exception, `Err`, failed `Future`, and the recovery around it |111| State transitions | Called twice, called out of order, called after close/dispose |112| Concurrency | Cancellation, timeout, racing callers — only where the code is actually concurrent |113114Two rules that decide most of the case list:115116- **One behavior per test.** A test that asserts five things fails with one message and tells you117 nothing about the other four.118- **A case earns its place by being able to fail.** If no plausible implementation change makes the119 test go red, it is not testing anything — drop it.120121For a bug-fix regression test, invert the order: write the test that reproduces the bug first,122confirm it fails against the unfixed code (or explain why that is not possible), then keep it.123124Case-selection heuristics, naming, and fixture/mock/fake choice are in125[test-design.md](references/test-design.md) — load it when the cases are not obvious from the126contract.127128### Step 3: Place the file correctly129130Wrong placement is the most common way a generated test never runs. The stack reference gives the131exact rules; the shape is always the same:132133- Mirror the source path in the test source root, and name the file after the unit under test134- Pick the source set by what the test needs: shared/pure logic goes in the common or plain unit135 test set; anything needing a device, a real framework, or a UI goes in the platform/instrumented136 set137- Match the existing suite's package/module declaration138139### Step 4: Write the tests140141Follow the conventions found in Step "Prerequisites 2" and the idioms in the stack reference:142143- **Arrange, act, assert** — visibly separated, in that order144- **Descriptive names** — the name states the condition and the expected result, so a failure is145 readable without opening the file146- **Assert the behavior, not the implementation** — asserting on internal calls freezes the design147 and breaks on every refactor148- **Substitute only what you must** — a real object beats a fake, a fake beats a mock; mocking149 types you own is usually a design smell150- **Deterministic by construction** — inject the clock, seed the random source, control the151 dispatcher/scheduler; never `sleep` to wait for async work152- **Fixtures over duplication** — but a little duplication in tests is cheaper than a fixture153 hierarchy nobody can follow154155Write the tests the project's existing suite would recognize as its own.156157### Step 5: Run them158159A generated test that was never executed is a draft. Run the suite — narrowed to the new tests160first, then the full file — and fix what fails.161162The stack reference has the exact command. Keep the output small: filter to failures rather than163pasting the whole run.164165Then verify the tests are worth having: change the implementation in a way that should break a166test (mentally, or actually and revert) and confirm the right test would catch it. Report honestly167if a test passes against a deliberately broken implementation — that means it is asserting nothing.168169### Step 6: Report coverage and the gaps170171Run the project's coverage tool if it is configured (Kover, `flutter test --coverage`, `xccov`,172`cargo llvm-cov`, `gcov`/`llvm-cov`, or the JS/Python equivalents — see the stack reference). If it173is not configured, do not add it unasked; report the gaps by reading the branches instead.174175Report the gaps, not the percentage:176177```markdown178## Tests added179<file paths, and one line per behavior covered>180181## Verification182<command run, and the real result>183184## Still untested185<specific branches or paths, as `file:line` — with why, if there is a reason>186```187188Coverage is a way of finding untested branches, not a target. Do not add tests whose only purpose189is to raise the number.190191## Operations192193| Operation | User intent | Output |194|---|---|---|195| `unit` (default) | "write tests for this function/class" | Isolated tests with dependencies substituted, run and green |196| `integration` | "test this end to end", "test the module together" | Tests across real collaborators — DB, HTTP, filesystem — with setup/teardown |197| `ui` | "test this screen/widget/view" | UI tests via the stack's UI harness; routes to the dedicated skill where one exists |198| `regression` | "write a test for the bug I just fixed" | One focused test that fails without the fix |199| `coverage` | "what is not tested" | Coverage run plus a ranked list of untested branches; tests only if asked |200201## Critical constraints202203- **Never assert on invented behavior.** Read the implementation; if the intended behavior is204 genuinely ambiguous, ask rather than encoding a guess as an assertion.205- **Never write a test you have not run.** Report failures rather than hiding them.206- **Do not modify production code to make a test pass** — that inverts the point. Propose the207 change separately if the code is untestable.208- **Do not rewrite or "improve" the existing test suite** while adding tests to it.209- **Do not delete or weaken a failing test** you did not write. A red test is information.210- **No sleeps, no wall-clock dependence, no network in unit tests.** Flaky tests are worse than211 missing ones because they train people to ignore red.212- **Do not chase a coverage number.** Assertion-free tests and getter tests inflate coverage and213 defend nothing.214- **Never put real credentials, tokens, or production data in fixtures.** Test data is committed215 and public.216217## References218219- [test-design.md](references/test-design.md) — cross-cutting case selection, naming, fixtures vs fakes vs mocks, and how to read a coverage report220- [kotlin-kmp.md](references/kotlin-kmp.md) — `kotlin.test` and JUnit, source sets, `runTest`/`TestDispatcher`, Turbine, MockK, Kover221- [flutter-dart.md](references/flutter-dart.md) — routing to the dedicated Dart/Flutter skills, plus goldens and coverage222- [swift-apple.md](references/swift-apple.md) — Swift Testing, XCTest, XCUITest, async tests, `xccov`223- [rust-cpp.md](references/rust-cpp.md) — `cargo test`, doc-tests, `proptest`, `criterion`, `llvm-cov`; GoogleTest, Catch2, CTest224- [other-languages.md](references/other-languages.md) — Jest/Vitest, pytest, Go `testing`, plain JUnit