Generate Tests for AI-Written Code
Ask AI for tests and you get a green suite that only checks the happy path and agrees with the code's bugs. Here's how to generate behavior-focused tests: give the model the intended behavior as the source of truth, cover boundaries and errors, and review them yourself.
Build a Test Generation PromptWhen "write tests for this" ships the bug green
You had the AI write a function, it looked right, and to be safe you added "now write tests for it." Back came a tidy test file, you ran it, everything passed — and it felt like proof. It usually isn't. The tests it wrote hit the happy path and stopped, asserted that the function "returns a number" instead of the right number, and — the quiet one — were written by reading the implementation, so they agree with whatever the code already does, bug included. The suite is green, and the bug it was supposed to catch is still there, now wearing a passing test as camouflage.
Getting useful tests out of AI is a different request from "write tests for this," because the same model that wrote the code will, if you let it, write tests that confirm the code rather than the requirement — repeating its own assumption in both places. The fix is to make the test prompt about behavior: give it the intended behavior and the edges as the source of truth, ask for the categories a real suite has, and make each test say what bug it would catch. This guide is how to generate tests for AI-written code that prove behavior instead of echoing it. NewPrompt builds that test prompt for you — the Test Case Prompt Generator writes the coverage rules and the non-goals that stop the model from "improving" your code instead of testing it — and it is honest about the limit: it doesn't run the tests, measure coverage, or check that anything passes. You paste the code and the behavior into your own AI tool, and what comes back is a draft test set you run and review yourself. Passing tests buy you some confidence; they are not a proof the code is correct.
Why "write tests for this" is weak for AI-written code
A test is only worth something if it can tell right from wrong independently of the code — and that is exactly what "write tests for this" gives up. Point a model at fresh code and ask for tests, and it does the natural thing: it reads the code, infers what the code is "supposed" to do from what it does, and writes tests that pass. Here is where that goes wrong, and why AI-written code makes it worse:
- The tests are derived from the implementation, not the requirement. A test that learned "correct" by reading the code will pass on the code's bug, because the bug is now the definition of correct.
- The same assumption gets encoded twice. If the model misread the spec when it wrote the code, it will misread it the same way when it writes the tests — the two agree, and both are wrong.
- Happy path looks like coverage. A green row for the normal case reads as "tested," while the boundary, the invalid input, and the error path — where the bugs actually live — go untouched.
- Assertions get weakened to pass. "Returns a discounted total" is easy to satisfy and proves nothing; the exact expected value is what a real test pins down.
- Mocks quietly replace the behavior under test. Over-mock a dependency and the test exercises the mock, not the code — green, and meaningless.
- Nobody says what each test proves. Without a stated purpose, a test is just an assertion that ran; you can't tell a meaningful check from a tautology by looking at the pass count.
Step 1: Give it the behavior, not just the code
The single change that makes AI tests worth running is handing the model the intended behavior as the source of truth, kept separate from the code. Alongside the function, give it the acceptance criteria or requirements in plain terms, the constraints that must always hold, a few concrete examples of input and expected output, the edge cases and failure modes you already know about, the framework and language, and what must not change. Now the tests have something to check the code against that isn't the code. When the requirement says "no discount below 100" and the implementation applies it at 99, a test anchored to the requirement goes red — which is the entire point.
The Test Case Prompt Generator builds this contract — you pick the strategy (unit, integration, edge case, regression), toggle the coverage areas, and it writes the rules, plus a non-goals section that tells the model not to rewrite or "improve" your implementation and not to weaken assertions to make tests pass, and an assumptions section that makes it separate what it knew from what it guessed and list the gaps it couldn't test. It builds the prompt; your own AI writes the draft tests, and you run them. NewPrompt doesn't execute anything.
Step 2: Tell it to test behavior, not mirror the implementation
Say the quiet part out loud in the prompt: do not read the implementation to decide what "correct" means — use the behavior spec. This one instruction is what breaks the loop where the test just restates the code. Go further and name the likely bug: "include a test that would fail if the threshold is applied at the wrong boundary," or "add a test that fails if the discount can push the total below zero." Framing a test as "this should fail when [specific bug] is present" forces the assertion to be specific enough to actually catch that bug, instead of the vague "it returns something" that passes either way. A test you can describe by the failure it prevents is a test that earns its place.
The Edge Case Test Prompt is this discipline aimed at the boundaries, where AI-written code is weakest — it skips edges by temperament. It partitions the input space and prompts for tests on both sides of every boundary (just-inside passes, just-outside fails — one without the other proves nothing), treats malformed and wrong-type inputs as first-class cases, and where the code leaves a boundary ambiguous, asks for tests of both readings and flags it rather than choosing one for you. Like the generator, it carries the non-goals that keep the model from editing your code to fit the tests.
Step 3: Ask for the categories a real suite has, not just the happy path
A happy-path test proves the code works when nothing goes wrong, which is the one case you didn't need a test for. Name the categories you actually want covered: the happy path, boundary values on both sides of every threshold, invalid and malformed input, the edge cases specific to this function, the error and exception paths, and a regression test for any bug you already fixed or specifically fear. If the function crosses a real seam — a database, an API, a queue — say whether you want that boundary exercised or mocked, and why. Making the categories explicit turns "some tests" into a checklist the model has to complete, so the invalid-input and error-path tests stop being the ones it forgets.
The Test Case Generator resource is a ready QA-engineer prompt organized exactly this way — happy path, boundary values, error and failure cases, permission and authorization, concurrency and state, and regression risks — and it asks for the future failure modes, the edge cases likely to break in a later refactor, not just today's behavior. It returns test scenarios rather than finished code, and flags which cases will need mocks or test doubles, so you decide the test infrastructure instead of the model deciding it for you. You still write the runnable tests from the scenarios, and you run them.
Step 4: Make every test say what it proves
Ask for more than code. For each test, have the model state the name, the scenario, the setup, the expected result, why the test matters, and — the field that does the most work — what bug it would catch. That last one is the tautology detector: a test whose "bug it would catch" comes back as "none, really" or "catches the code not doing what the code does" is a test that proves nothing, and now you can see it. A test that says "catches a threshold applied at the wrong boundary" is one you can trust to mean something when it is green, because you know what its red would have meant.
This is also what makes the suite reviewable by someone who wasn't there when it was written. A wall of assertions tells you nothing about intent; a list where each test names the failure it guards against tells you immediately where the coverage is real and where it is decoration. When two tests claim to catch the same bug and none covers the invalid input, the gap is visible — which is the first thing you would want to know before trusting the green.
Step 5: Review the tests, then run them yourself
Read the draft before you trust it, with three questions. Does each test assert the requirement, or does it just restate what the code does? Could any test pass for the wrong reason — a weak assertion, or a mock that stands in for the very behavior you meant to check? And do the tests cover the categories you asked for, or did the invalid-input and boundary cases quietly go missing? A test suite generated from a prompt is a draft with the same failure modes as any AI output, and it needs the same review.
Then run them, because a test that hasn't run is a guess about a guess. Failing tests are information, not a verdict: a red test means the code is wrong or the expected value is wrong, and reading which is your job — sometimes the test encodes a misunderstanding and the code is fine. Passing tests are limited confidence, not correctness: they prove the cases you tested behave as asserted, and say nothing about the cases you didn't write or whether your assertions match the real requirement. On anything high-risk, the generated set is a starting point for a real test strategy an engineer owns — not the strategy itself. NewPrompt writes the prompt; running the tests, interpreting the failures, and deciding what "enough coverage" means stay with you.
Common mistakes
The habits that produce a green suite you can't actually trust:
- Asking for tests from the code alone. Give the model the intended behavior too, or it will test what the code does instead of what it should do.
- Letting the tests mirror the implementation. Tell it to use the behavior spec, not the code, to decide what "correct" means — otherwise the tests inherit the code's bugs.
- Stopping at the happy path. The boundary, the invalid input, and the error path are where the bugs live; name those categories explicitly.
- Accepting vague assertions. "Returns a value" passes on almost anything — demand the exact expected result and what bug the test would catch.
- Over-mocking the thing under test. If a mock replaces the behavior you meant to check, the green proves the mock works, not the code.
- Treating a passing suite as correctness. Passing tests are confidence on the cases you wrote, not a proof the function is right — and you still have to run them.
A worked example: tests that could actually go red
Watch "write tests for this" confirm a discount function's bug, then a behavior-focused prompt write a test that would catch it.
"Write tests for this" confirms the code at total = 100 and misses the boundary; a behavior-anchored prompt writes a test at 99.99 that goes red on the off-by-one a happy-path test never reachesTHE AI-WRITTEN FUNCTION:
calculateDiscount(total, customerType)
THE INTENDED BEHAVIOR (the source of truth -- not the code):
- regular customers: no discount below 100
- regular customers: 10% off at 100 and above
- vip customers: 15% off at 100 and above
- the discount must never make the total negative
- an invalid total is an error (throw, per our convention)
THE WEAK PROMPT, AND WHAT IT GETS BACK:
"Write tests for this discount function."
- one test: regular customer at 100 -> gets a discount
- asserts the function "returns a discounted number", not the exact value
- no test just below 100, no VIP test, no invalid input
- written by reading the code, so it agrees with the code's bug
A BEHAVIOR-FOCUSED PROMPT (behavior spec is the reference, not the code):
Generate tests for this function from the intended behavior below --
do NOT read the implementation to decide what is correct.
For each test: name | scenario | expected result | what bug it would catch.
Cover: below-100 boundary, exactly 100, VIP vs regular, invalid total,
and a regression test that the total never goes negative.
ONE TEST, WRITTEN TO CATCH A BUG:
Test: regular_customer_at_99_99_gets_no_discount
Scenario: total = 99.99, customerType = "regular"
Expected: returns 99.99 (no discount)
Catches: an off-by-one where the threshold fires at > 99, or rounds
99.99 up to 100 -- the exact boundary bug a happy-path test
at total = 100 never sees.
NEXT: run these in your own project. A red test here means the code is wrong
OR the expected value is -- you read which. Green on these five is more
confidence than "write tests for this" gave you, not a proof the function
is correct.
Where this fits in NewPrompt
Generating tests is one job in the coding family, and it does its job only when the tests can disagree with the code. The Test Case Prompt Generator builds the test-generation contract — strategy, coverage areas, non-goals, assumptions. The Edge Case Test Prompt aims it at the boundaries where AI-written code is thinnest, and the Test Case Generator resource lays out the categories a systematic suite covers. Each hands you a prompt you run in your own AI tool, on your own code; NewPrompt writes the structure, and the tests run on your side, where you read the results.
The neighbors do related but different jobs, and reaching for the wrong one is its own mistake. Reviewing AI-written code reads it and reports what looks wrong — a judgment, with nothing executed; generating tests builds executable checks that prove behavior on specific inputs when you run them. The review finds the gaps; the tests build the net over them, and you often want both. Debugging is for when a test is already red and you need to know why — a different question from "how should this be validated." A QA checklist enumerates what to think about before shipping; test generation turns the relevant items into executable checks that actually run. And testing assumes you already know the intended behavior; it is how you check the code meets it, not how you work out what it should be.
A test earns its green by being able to go red. A check that would pass no matter what the code did is a light wired to nothing — reassuring and meaningless — and the fastest way to build a suite full of them is to let the model write tests by reading the code it just wrote. The way out is to test against the behavior you meant, name the bug each test guards against, and then run the suite yourself and read what the reds are telling you. The model can draft the checks; whether green means what you think it means is a call you make, not one the passing count makes for you.