TL;DR: A unit test prompt generator is a template you fill in with your language, framework, assertion style and mocking approach, then paste your function underneath, not a hosted button. This page gives verified templates for Jest 30, pytest 9, JUnit 6.1 and Go plus testify, and covers five ways AI-written tests fail silently: asserting implementation instead of behaviour, passing trivially, over-mocking, missing edge cases, and flakiness.
There is no hosted "generate my unit tests" tool on this page, or anywhere on Prompt Architects. What follows is a copy-paste unit test prompt generator: a template with blanks for language, test framework, assertion style and mocking approach, built so the model can't quietly default to whatever it saw most often in training. You fill it in, paste it into ChatGPT, Claude or Gemini alongside your function, and read what comes back before you commit it.
That reading step is not optional, and it is the actual subject of this page. A prompt that says "write unit tests for this" gets you something that runs. It does not get you something that catches the next bug. The gap between those two is framework-specific syntax you can look up, but it is mostly a set of failure modes that show up identically whether the model is writing Jest, pytest, JUnit or Go, and those are the ones worth understanding before you copy anything below.
None of this requires trusting a vendor's claim about what its model can do. Every code sample on this page, the Jest matchers, the pytest fixture syntax, the JUnit Jupiter assertions, the testify mock pattern, was pulled from that framework's own current documentation while writing this page, not reconstructed from memory of an older release. Framework maintainers change assertion helpers and mocking APIs between major versions, so a prompt built on last year's syntax can quietly ask a current model for a method that no longer exists, or that exists under a different name.
What Goes Wrong When a Prompt Doesn't Name a Framework?
"Write unit tests for this function" leaves four decisions to the model, and it will make all four for you, silently, and possibly differently on your next run: which framework, which assertion style, what to mock, and how many cases to cover. None of those are safe defaults.
Framework matters because the assertion syntax is not interchangeable: expect(x).toBe(y) is Jest and Vitest, assert x == y is Python, assertEquals(y, x) is JUnit, and assert.Equal(t, y, x) is testify for Go, and a model with no instruction will pick whichever it saw most in similar-looking code, which is not necessarily what your project runs. Assertion style matters inside a framework too: Jest's toBe does strict equality and its toEqual does deep equality, and a test using the wrong one for an object will either false-fail or silently pass a broken deep-equality check. Mocking approach matters because an unscoped instruction to "mock the dependencies" will mock everything reachable, including things that should run for real. And case count matters because a model asked for "some tests" reliably stops at the happy path plus one obvious failure, which is exactly the coverage a code reviewer would reject.
None of this is a reason to avoid naming a framework version specifically, either. "Write Jest tests" and "write Jest 30 tests" produce noticeably different output when the two versions disagree on a default: Jest's global functions and matcher set have stayed stable for years, but a mocking helper introduced in one release and a configuration default changed in another are exactly the kind of detail a model trained across many versions will blend rather than pick correctly. Naming the version costs four characters and removes the ambiguity.
The fix is naming all four up front, which is what the base template below does.
The Base Template: Language, Framework, Assertion Style, Mocking
Copy this, fill in the brackets, delete what you don't need, and paste your function or class underneath.
You are writing unit tests for the code below. Test the observable behaviour,
not the internal steps it takes to get there.
LANGUAGE: [JavaScript / TypeScript / Python / Java / Go]
TEST FRAMEWORK: [Jest 30 / Vitest 4 / pytest 9 / JUnit 6.1 + Mockito / Go testing + testify]
ASSERTION STYLE: [expect().toBe() and .toEqual() / plain assert / assertEquals() / assert.Equal()]
MOCKING APPROACH: [jest.mock() / vi.mock() / unittest.mock or the mocker fixture / Mockito @Mock / testify/mock]
WHAT THIS CODE DOES: [one or two sentences, in your own words]
PUBLIC CONTRACT: [inputs, return value, every error/exception it can produce]
WHAT NOT TO MOCK: [pure functions and value objects — mock only real I/O, the clock, and randomness]
REQUIRE, across the test file:
- One test for the documented happy path, using a realistic, non-trivial input.
- One test per boundary value: empty input, zero, a negative number, the
maximum allowed length or size.
- One test per documented error or exception path, asserting the SPECIFIC
error or message, not just that something was thrown or returned.
- At least one test that would fail if the implementation were swapped for a
different-but-plausible one — this is the anti-trivial check. If every test
would still pass under a subtly wrong implementation, add assertions on
concrete expected values, not just on the type or presence of a result.
- No assertions on private state, internal call counts, or helper functions
not part of the public contract.
If you cannot write a case without seeing more of the surrounding code, say so
and name exactly what you need instead of guessing at it.
CODE:
[paste here]
That block is doing three jobs at once: naming the four decisions from the section above, forcing edge-case coverage instead of letting the model stop at the happy path, and explicitly banning two of the failure modes covered next, implementation coupling and over-mocking.
What Makes a Generated Test Pass When It Shouldn't?
This is the part a coverage number will never show you. Four failure modes, all of them produce a test suite that runs green, and all four are things a model does by default unless the prompt above stops it.
It asserts the implementation, not the output. The clearest version of this: a test that recomputes the exact formula the function under test uses, rather than checking a known expected value.
// A test that passes no matter what the formula computes — it just re-derives it
function applyDiscount(price, pct) {
return price - (price * pct) / 100;
}
test('applies discount', () => {
const price = 100, pct = 20;
expect(applyDiscount(price, pct)).toBe(price - (price * pct) / 100);
});
If someone changes the operator inside applyDiscount tomorrow (say, subtracting the wrong side of the equation), this test still passes, because it copies the same arithmetic rather than checking a fixed answer. The fix is a literal expected value:
test('applies a 20% discount to a $100 price', () => {
expect(applyDiscount(100, 20)).toBe(80);
});
test('a 0% discount returns the original price', () => {
expect(applyDiscount(100, 0)).toBe(100);
});
It passes trivially. A cousin of the above: an assertion so loose it can't fail. expect(result).toBeDefined() or assert result is not None passes for almost any non-crashing output, including a wrong one. Ask specifically for equality assertions against concrete values, not presence checks, except where "did not throw" genuinely is the whole contract.
It over-mocks. The failure here is subtler: mock every collaborator a function calls, and the test stops exercising the function's own logic at all.
# Every dependency the function touches is mocked — the test can't fail
def test_send_welcome_email(mocker):
mocker.patch("app.mailer.render_welcome_email", return_value="Welcome!")
mocker.patch("app.mailer.send", return_value=True)
result = send_welcome_email(user)
assert result is True
If send_welcome_email is just "render, then send," both calls are stubbed and result is whatever the last mock was told to return: the test passes whether the function calls render before send, passes the right user data, or does nothing useful between the two calls. The fix is mocking only the real I/O boundary and asserting on what got passed into it:
def test_send_welcome_email_renders_and_sends_to_the_right_address(mocker):
mock_send = mocker.patch("app.mailer.send")
send_welcome_email(user)
mock_send.assert_called_with(to=user.email, subject="Welcome", body=mocker.ANY)
Now the test actually checks that send_welcome_email wires the pieces together correctly, instead of just confirming that two mocks exist.
It misses edge cases the happy path never touches. Empty input, a negative number where the domain assumes positive, the maximum length a field allows, a duplicate in a collection expected to be unique, and every documented error path: none of these show up unless the prompt asks for them by name, which is why the base template lists them explicitly rather than trusting "and edge cases too" as an afterthought.
It's flaky, which reads as a false failure and trains the team to stop trusting the suite. A model asked to test a function that touches the current time, a random number, or a real network call will happily write an assertion against whatever value happened to come back during generation, not against the class of values the function can actually produce.
// Flaky: passes only if the test happens to run at the same millisecond
test('formats the current timestamp', () => {
expect(formatTimestamp()).toBe('2026-09-02T10:14:22.101Z');
});
The fix is naming the non-determinism in the prompt so the model controls for it instead of hardcoding a snapshot of it, freezing the clock, seeding the random generator, or mocking the network boundary rather than asserting on its live output:
// Deterministic: the clock is frozen for the duration of the test
test('formats a fixed timestamp', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-09-02T10:14:22.101Z'));
expect(formatTimestamp()).toBe('2026-09-02T10:14:22.101Z');
jest.useRealTimers();
});
How Do You Prompt Each Framework So the Syntax Is Actually Right?
The base template covers structure. These four cover the syntax each framework actually uses, verified against each project's own documentation as of September 2026, not remembered from an older version.
| Feature | Jest 30 (JS/TS) | pytest 9 (Python) | JUnit 6.1 (Java) | Go testing + testify |
|---|---|---|---|---|
| Assertion style | expect(x).toBe(y) | assert x == y | assertEquals(y, x) | assert.Equal(t, y, x) |
| Mocking approach | jest.fn(), jest.mock('module') | unittest.mock.patch, or pytest-mock's mocker | Mockito: @Mock, when(...).thenReturn(...) | testify/mock: embed mock.Mock, .On(...).Return(...) |
| Run command | npx jest | pytest | mvn test | go test ./... |
| Failure output | expect(received).toBe(expected) | assert with a printed diff | ComparisonFailure with expected/actual | t.Errorf with your own message |
JavaScript and TypeScript (Jest 30 or Vitest 4)
Write Jest 30 tests. Use expect(x).toBe(y) for primitives and
expect(x).toEqual(y) for objects and arrays — toBe on an object compares
identity, not contents, and will false-fail. Mock only network and time;
use jest.mock('module-name') for a whole module or jest.fn() for a single
callback. Do not mock the function under test.
Cover: the documented happy path with a realistic input, an empty
array/string, a boundary value, and the documented rejected-promise or
thrown-error path — assert the specific message, not just that it rejected.
FUNCTION:
[paste]
Swap jest.mock / jest.fn for vi.mock / vi.fn if the project runs Vitest instead, since the expect() matcher syntax is close enough between the two that the rest of the prompt does not change.
Python (pytest 9)
Write pytest 9 tests using plain `assert` statements — no unittest.TestCase
subclassing. Use @pytest.fixture for setup shared across tests, not repeated
setup code in every test body. Mock only I/O boundaries with
unittest.mock.patch or the mocker fixture from pytest-mock; never mock the
function under test itself.
Cover: the happy path, an empty or None input, a boundary value, and the
documented exception using pytest.raises(SpecificExceptionType) — not the
bare Exception class.
FUNCTION:
[paste]
Java (JUnit 6.1 + Mockito)
Write JUnit 6.1 (Jupiter) tests: org.junit.jupiter.api.Test on each method,
Assertions.assertEquals for value checks, Assertions.assertThrows for the
documented checked or unchecked exception. Mock collaborators with Mockito:
@Mock fields under @ExtendWith(MockitoExtension.class), when(...).thenReturn(...)
for stubbing, and verify(...) only for interactions that are part of the
contract — not for implementation-only calls nobody outside the class cares
about.
Cover: the happy path, a null or empty argument, a boundary value, and the
documented exception type.
CLASS:
[paste]
Go (standard testing package + testify)
Write table-driven Go tests using the standard testing package:
func TestX(t *testing.T), one t.Run(caseName, func(t *testing.T) {...}) per
case, t.Errorf or t.Fatalf on failure. If the function depends on an
interface, mock it with testify/mock — a struct embedding mock.Mock, calls
recorded with m.Called(...), expectations set with .On(...).Return(...), and
testObj.AssertExpectations(t) at the end of the test. Do not fake a concrete
struct that has no interface boundary; that hides the real dependency
instead of substituting it.
Cover: the happy path, a zero-value input, a boundary value, and the
documented error return — Go returns errors rather than throwing, so assert
on the returned error value, never on a panic unless the contract says the
function panics.
FUNCTION:
[paste]
If you also want the model to review code rather than only write tests for it, How to Prompt for a Genuinely Useful Code Review covers the same specificity problem from the review side, and the 35-prompt code review and debugging pack has ready prompts for the surrounding workflow.
What Should a Coverage Checklist Actually Check?
Not a percentage. A coverage percentage counts which lines executed during the run, not whether the assertions on those lines check anything meaningful: a suite built entirely from the trivial and over-mocked examples above can report 100% and verify nothing. Coverage tooling is genuinely useful for finding code nobody has tested at all, a dead branch, an untouched error handler, but a green coverage number and a trustworthy test suite are two different claims, and only one of them is checked by a coverage tool. Run this checklist against the test file instead of a target percentage:
| Check | What it catches |
|---|---|
| Happy path uses a realistic, non-trivial input | Tests written against 0, 1, or an empty string as the "normal" case, which hides bugs that only appear with real data |
| Every boundary value has its own test | Zero, negative, empty, and maximum-length inputs, each asserted separately rather than lumped into one case |
| Every documented error path has its own test | A specific exception type or error value asserted, not a generic "it throws" check |
| No assertion checks presence only | toBeDefined(), is not None, and similar checks that pass for almost any non-crashing output |
| Mocks cover I/O only | Network, filesystem, database, clock, and randomness, not pure functions or value objects |
| At least one test would fail under a plausible wrong implementation | The anti-trivial check from the base template, applied by hand if you're unsure |
| You have read every assertion | Not just confirmed the suite is green |
Should AI Write All of Your Tests, or Just the First Draft?
Just the first draft, for anything the answer to a bug report would embarrass you to get wrong. AI is genuinely strong at the mechanical part of this job: enumerating boundary values you'd otherwise forget, naming the exception path buried in documentation you skimmed, and writing the repetitive assertion boilerplate for four similar cases in a row. None of that requires understanding your business.
What it cannot do reliably is know your business rules well enough to invent the right test data on its own. A prompt that says "test the discount function" has no way to know that your actual policy caps a discount at 50% regardless of what the formula computes, unless you put that rule in the prompt yourself. It also has no visibility into how your code behaves under real concurrent load, no matter how carefully it writes a table-driven test for a single call. Complex integration scenarios spanning several services are the same story: a unit test, generated or not, tests one unit, and a model asked for unit tests will not volunteer that your real bug lives in the interaction between two of them.
The workable split is the model drafts, checked against the base template's requirements, and a human who understands the business rule owns the final read before it merges. That is not a lower bar than writing tests by hand; it is the same bar, applied to a first draft that arrives faster than a blank file does.
Where Does This Fit Next to Debugging and Refactoring Prompts?
Writing the tests is one half of a workflow that also includes catching what's already broken and cleaning up what already works. 30 AI Prompts for Debugging covers the failure-analysis side (stack traces, bisecting, reproduction) for when a test you wrote here starts failing and you need to find out why. 40 AI Prompts for Software Development covers the surrounding lifecycle, from requirements through CI/CD, if tests are one step in a longer prompt chain you're building. And if the function under test is undocumented enough that writing its contract is half the work, the free code documentation prompt generator covers writing that contract down first.
None of this needs an account. Every template above runs as-is in ChatGPT, Claude or Gemini. If you want the wording of a prompt like these refined before you send it, tightened phrasing rather than different content, that's what our Chrome extension does inside the same chat window, on Pro and above. The free plan gives 5 prompt enhancements a day, forever, with no credit card required, per our FAQ page. Current pricing is on the pricing page.
Stop rewriting prompts. Start shipping.
Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 4.8★ on the Chrome Web Store.
Create An Account