When AI writes tests, the common wreck isn't "can't write any"--it's "writes a pile of useless ones." Coverage numbers look good, the core path isn't covered; mocks are so heavy you're testing the mock, not the code; boundary cases are guessed and miss the inputs that actually break things. The problem isn't the model, it's that the prompt never pinned down "what to test, how to test it, how far to go."
This site has two companion pieces: the General Coding Prompt Pack (prompt-coding-pack) covers code generation and refactoring; the Code Review and Debug Prompt Pack (prompt-code-review-debug-pack) covers review and debugging. This one handles exactly one thing--"writing tests," for code that already runs and needs coverage. The three don't overlap: those two produce code or diagnoses, this one produces test cases, test plans, and test strategy. If you want "test generation as an automated pipeline," see the AI Test Generation SOP (ai-test-generation-sop); this pack is for "a human with a prompt, filling in tests section by section."
Four prompts in three escalating tiers, plus a speed cheatsheet. Each has a role, task, constraints, and output format, with variables marked {{}}. Copy and adapt.
1. Beginner: Generate Unit Tests for a Function
The wreck starts at "let the AI just write tests." It defaults to 5 happy-path cases with assertions like assert result is not None, touching no boundary. This step forces "signature and boundary list first, cases second," turning test generation into two turns: align the contract, then produce cases.
You are a senior {{language}} test engineer. Generate unit tests for the following function, in two steps. Do not write the full set in one go.
Function code:
{{function code}}
Test framework: {{pytest | jest}}
Step 1 -- Function signature and boundary list (write only this, wait for my confirmation before writing cases)
- Function name, parameter types, return type
- For each input, list the valid range and boundary values (null / empty string / empty array / 0 / negative / oversized / huge / wrong type)
- List every exception or error branch to verify (throws / returns error code / returns default), stating the trigger condition
- Do not write test case code
Step 2 -- Test cases (after I confirm the boundary list)
- Write {{N}} cases each for normal / boundary / error, {{3N}} total
- Each case: case name (describing the scenario under test), input, expected output or expected exception
- Use {{pytest}} parameterization (@pytest.mark.parametrize) or {{jest}} test.each; do not write each case as a standalone test function
- Use only interfaces that actually exist on the function under test and the standard library; do not invent any API or helper that doesn't exist. If unsure whether an assertion method exists, mark it "to verify"
- Do not write `assert result is not None` style non-assertions; every assertion must pin a concrete value or structure
Constraints: if the boundary list has gaps or ambiguity, ask me first; do not fill them in yourself. Tests must run directly, with no skip or TODO placeholders.Key point: the two-turn structure is the soul of this prompt. The AI skips boundary analysis and piles on happy-path by default; forcing it to list boundaries first is how you catch that it never considered "empty array"--the input that triggers division by zero. "Every assertion pins a concrete value" is the other gate: assert result is not None is the number-one AI test smell, equal to not testing at all.
2. Intermediate: Parameterized Batch Cases and a Test Plan
A single function's tests are easy; a whole module's need planning. Telling the AI "write tests for this module" makes it pick the easy functions and pile on cases, barely touching the genuinely complex core path. This step splits testing into "parameterized batch generation + test plan + mock strategy," forcing it to inventory gaps before writing.
You are a senior test architect. Produce a test plan and parameterized cases for the following module, in three parts.
Module code:
{{module code}}
Test framework: {{pytest | jest}}
Existing tests: {{existing test files, or "none"}}
1. Test plan
- List every public function/method in the module, marking current coverage state (covered / partial / uncovered)
- For each uncovered or partial function, list the case types needed (normal / boundary / error / concurrency / idempotency)
- Mark which functions are core paths (high blast radius if broken) and should be tested first
- Produce a coverage gap list: which branches, error paths, and boundaries are not touched by any existing case
2. Parameterized cases
- For core-path functions, generate cases in parameterized form (@pytest.mark.parametrize or test.each)
- For each parameterized group: parameter names, an input combination table (at least 5 rows, spanning normal + boundary + error), and the expected output per row
- The parameter table must include easily-missed combinations: empty collection vs single-element, 0 vs negative, huge values, wrong types, off-by-one boundaries
3. Mock strategy
- List the module's external dependencies (database / network / file IO / time / third-party API)
- For each dependency: mock, stub, or fake, and why; the mock scope (function-level / module-level)
- Mark which dependencies should NOT be mocked (e.g. pure computation functions), and why
- State the minimal-mock principle: mock only dependencies with side effects; use the real implementation for pure functions
Constraints: do not invent functions or attributes that don't exist on the module under test; mocks must be based on real dependency interfaces--if a dependency interface isn't visible in the code, mark it "dependency interface to verify"; tests must not depend on network or a real database and must run locally.Key point: the "coverage gap list" is the key difference from the beginner prompt--beginner handles "cases for one function," intermediate handles "gap audit for a module." The AI dodges hard-to-test functions (async, side effects, many dependencies); forcing it to list "uncovered" states is how you see where it slacked off. "Minimal-mock principle" counters the AI flavor: the AI mocks every dependency, so you end up testing the mocks while the real code never runs.
3. Expert: Integration/E2E Strategy, Coverage Analysis, and Test Refactor
Unit tests are filled in; integration and E2E still need layering. The AI makes two mistakes here: it writes integration tests as oversized unit tests (mocking all dependencies, still testing a single point), and it keeps piling tests without ever pruning redundancy. This step forces "test pyramid layering + contract testing + redundancy pruning," producing a test system rather than a pile of test files.
You are a test architect. Produce an integration/E2E strategy and a test refactor plan for the following project, in three parts.
Project overview:
{{project structure + main modules + existing test situation}}
1. Integration and E2E strategy
- Layer by the test pyramid: unit (current coverage) / integration / E2E, with the rough proportion each layer should occupy
- Integration layer: list the cross-module interaction points that need verifying (module A calling module B's interface, data flow, event triggers), with 1-2 verification cases per interaction point
- E2E layer: list 3-5 core business flows (full path from entry to final output), with verification points for each
- Contract testing: if the project has inter-service calls (HTTP / message queue), list the interfaces that need contract tests, and whether to use Pact or self-built schema validation
- Mark which layers are currently missing and which should be filled first
2. Coverage gap analysis
- Based on existing tests, list the 3 modules with the thinnest coverage (estimate by branch or path coverage; do not give fabricated precise numbers)
- For each thin module, explain where the gap is (error branches / boundaries / concurrency / error recovery)
- Give a prioritization: sort by "blast radius x gap size"
3. Test refactor
- Find redundant cases: multiple cases testing the same branch, parameterizable merges, repeated setup extractable to fixtures
- Find fragile cases: order dependence, time dependence, network dependence, overly loose assertions (only asserting not None)
- Produce a pruning list: which cases can be deleted (with reasons), which can be merged, which should be rewritten
- State the expected effect after refactor: rough estimate of case-count reduction and runtime reduction (give rough estimates, not fabricated precise numbers)
Constraints: do not fabricate precise coverage percentages; layer proportions as ranges, not exact numbers; contract testing plans must be based on real project dependencies--if a dependency isn't reflected in the overview, mark it "to verify"; test refactoring must not change the behavior of the code under test.Key point: "no fabricated precise numbers" is the anti-fabrication gate of this prompt--the AI confidently says "coverage will rise from 62% to 87%," and a number with no source is fabricated; force it to give ranges and "rough estimates." "Prune redundancy" is the core difference from the prior two tiers: beginner and intermediate are both "adding tests," expert starts "removing tests"--redundant cases are the hidden debt of a test system, slow to run and costly to maintain, and the AI only adds by default, so you must force a pruning list.
Cheatsheet: Add Tests to a Snippet in 5 Minutes
Not every scenario calls for a test plan. You fixed a bug, added a small function, or noticed missing coverage in review--the core need is "5 minutes, a few runnable cases." This cheatsheet forces "boundary list + parameterized cases + the one path most worth testing," no big moves.
You are a test engineer. Produce a minimal runnable test set for the following code in 5 minutes.
Code:
{{code}}
Test framework: {{pytest | jest}}
Output in three parts, tight and no fluff:
1. Boundary list: 5 inputs most likely to break things (empty / single element / out of range / wrong type / extreme value), one line each
2. Parameterized cases: a parameterized test using @pytest.mark.parametrize or test.each, with 2 normal + 3 boundary cases, each with its expected output
3. The one path most worth testing: point out the execution path most likely to break in production, and write one assertion case targeting it
Constraints: use only interfaces that exist in the code; assertions must pin concrete values, no `assert not None`; if an interface is uncertain, mark it "to verify," do not fabricate.Key point: "the one path most worth testing" is the soul of this cheatsheet. The AI spreads cases evenly by default, but real code always has a "most fragile path"--maybe a boundary junction, maybe an error-recovery branch. Forcing it to name that path is more useful than 10 happy-path cases. "No assert not None" is the hard rule for de-AI-ing; non-assertions are the source of test debt.
Four Standing Constraints
The four prompts share four hard constraints. They are the floor for "whether AI-written tests are usable":
- Runnable + boundary handling: output tests must run directly, with no skip or TODO; boundaries (null, empty array, oversized input, concurrency) must have explicit cases, not waved off with "assume valid input" in a comment.
- No fabricated APIs: the number-one cause of AI test wrecks. It will confidently write
assertUserEquals(),mockDatabase.flush()--assertions or mock methods that look reasonable but don't exist. Allow only the test framework's standard API or interfaces that really exist on the code under test; mark anything uncertain "to verify." - AI draft needs human review: every prompt's output is a draft, not a finished product. AI-generated tests especially need a human to verify "whether the assertion actually locks down the right thing"--the AI often writes pseudo-tests that "pass but don't test the point." The prompt asks for a "boundary list" and "expected outputs" to give the human reviewer a handle.
- De-AI the output: ban non-assertions (
assert not None,assert True); ban filler comments ("this tests the normal case"); merge duplicate cases with parameterization, don't write one test function per input. AI-flavored tests look numerous but carry low information density.
How to Use
Pick by scenario:
- Unit tests for one function: use the beginner prompt. Get the signature and boundary list first, confirm, then parameterized cases.
- Test audit for a whole module: use the intermediate prompt. Get the test plan and coverage gaps first, then batch parameterized cases, with mock strategy decided separately.
- Building a test system or clearing test debt: use the expert prompt. Layer first (pyramid + contracts), then analyze coverage gaps, then list redundant cases to prune.
- Ad-hoc case fill-in: use the cheatsheet. 5 minutes for boundary list + parameterized cases + the one path most worth testing.
Model choice: beginner and the cheatsheet are low-bar; mainstream models handle them, with DeepSeek offering good cost-performance for batch test fill-in. Intermediate test planning and expert layering lean reasoning-heavy, where Claude Opus / GPT-5 / Codex-class coding-tuned models are more reliable, and Claude's long context suits feeding an entire module's code and existing tests in for gap analysis. One line: pick DeepSeek for everyday fill-in, Claude / GPT-5 for test-system design and refactor.
Pitfalls
- Asking for all tests in one shot: it piles on happy-path and touches no boundary. Fix: the beginner prompt forces two turns, boundary list before cases.
- Non-assertions everywhere: the AI loves
assert result is not None--looks covered, tests nothing. Fix: hard constraint "assertions pin concrete values," ban not-None-style assertions. - Over-mocking: the AI mocks every dependency, so you're testing the mock, not the code. Fix: the intermediate prompt forces "minimal-mock principle"--mock only dependencies with side effects, use real implementations for pure functions.
- The AI fabricates coverage numbers: it confidently says "coverage goes from 62% to 87%" with no source. Fix: the expert prompt hard-bans "fabricated precise percentages," allowing only ranges and rough estimates.
- Only adding tests, never pruning: tests pile up, slow to run, costly to maintain, never pruned. Fix: the expert prompt forces a "pruning list," actively making the AI find redundant and fragile cases.
References
- Anthropic, "Prompt engineering": docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview
- OpenAI, "Prompt engineering guide": platform.openai.com/docs/guides/prompt-engineering
- Martin Fowler, "TestPyramid": martinfowler.com/bliki/TestPyramid.html
- Pact, "Contract testing": pact.io