AI Prompts for Writing Unit Tests
89% test coverage and still shipped a bug that cost 12% on every order. This case study shows why generic AI unit test prompts miss the cases that matter — and the exact prompt fix that changed that.
Write unit tests for this Python function.
def calculate_order_discount(subtotal, customer_tier, bulk_quantity):
if customer_tier == "gold" and bulk_quantity >= 50:
return subtotal * 0.20
elif customer_tier == "silver" and bulk_quantity >= 30:
return subtotal * 0.10
elif bulk_quantity >= 100:
return subtotal * 0.05
return 0The Problem Marcus Faced
Marcus is a backend engineer at a logistics company. In Q3 last year, his team shipped a refactored inventory module that passed all existing tests. Four days after deployment, a subtle edge case in the discount calculation logic caused orders to be underpriced by 12%. They caught it in a finance reconciliation — not in tests.
The post-mortem identified the root cause: their unit tests had 89% line coverage but 0% coverage of the boundary conditions where the bug lived. They'd tested the happy path. They hadn't tested what happened when a bulk order crossed a discount tier threshold mid-calculation.
Marcus's response was to start using ai prompts for unit tests — specifically, prompts that generated adversarial test cases, not just the ones that were easy to think of.
The Wrong Approach
His first attempt was what most developers try:
Write unit tests for this Python function.
def calculate_order_discount(subtotal, customer_tier, bulk_quantity):
if customer_tier == "gold" and bulk_quantity >= 50:
return subtotal * 0.20
elif customer_tier == "silver" and bulk_quantity >= 30:
return subtotal * 0.10
elif bulk_quantity >= 100:
return subtotal * 0.05
return 0The AI wrote four tests. Happy path for gold tier. Happy path for silver tier. Happy path for bulk. One test for no discount. All passed.
None of them tested: what happens when a gold customer orders exactly 49 items (one below the threshold)? What about
bulk_quantity=NoneThe coverage number looked great. The test quality was not.
⚠️ Common mistake: Asking for "unit tests" without specifying test categories. The AI defaults to positive test cases. Bugs live in negative cases, boundary conditions, and unexpected inputs — and you have to explicitly ask for those.
The Correct Prompt
You are a QA engineer writing unit tests with a specific focus on finding edge cases and boundary conditions.
For the following Python function, write a complete pytest test suite. Include:
1. Happy path tests (2–3 covering main scenarios)
2. Boundary condition tests — test every threshold value (at, just above, just below)
3. Invalid input tests — None values, wrong types, empty strings, negative numbers
4. Unexpected value tests — customer tiers not in the conditions, quantities as floats
5. For each test: use a descriptive name that explains what scenario is being tested and what outcome is expected
After the tests, add a comment block listing any assumptions you made about expected behavior that aren't clear from the code — these are potential gaps in the function's spec.
Function:
def calculate_order_discount(subtotal, customer_tier, bulk_quantity):
if customer_tier == "gold" and bulk_quantity >= 50:
return subtotal * 0.20
elif customer_tier == "silver" and bulk_quantity >= 30:
return subtotal * 0.10
elif bulk_quantity >= 100:
return subtotal * 0.05
return 0
Language: Python 3.11, testing with pytestWhat this does: It explicitly tasks the AI with adversarial thinking — finding what the code doesn't handle. The "assumptions" comment block is the hidden gem here: it surfaces spec ambiguities that often indicate missing validation or undocumented behavior.
⚡ Pro tip: The assumptions block is often more valuable than the tests themselves. When the AI notes "I assumed
Nonebulk_quantityResults and What Changed
Marcus ran the improved prompt on the discount function. The AI generated 17 tests — including five boundary condition tests he wouldn't have written manually, and three invalid-input tests that revealed the function silently returning 0 for
NoneThat silent failure was exactly the class of bug that caused the Q3 incident. Different function, same pattern.
He also got a list of four assumption gaps — places where the spec wasn't clear. That list became the agenda for a 20-minute spec clarification meeting with the product manager, which uncovered two more edge cases that needed to be defined.
Across the team's next sprint, they ran the same prompt pattern on every new function. Test coverage didn't change much in percentage terms. But defect escape rate dropped by 40% in the quarter.
How to Apply This to Your Situation
The core framework — happy path + boundary conditions + invalid inputs + assumption gaps — works in any language. Here are three adapted versions:
JavaScript with Jest:
Write a Jest test suite for this function. Include: happy path tests, boundary tests at every conditional threshold, tests for undefined and null inputs, and tests for unexpected types (string where number is expected, array where primitive is expected). Use descriptive test names in the format: "should [expected behavior] when [condition]".What this does: Jest's
describeitJava with JUnit 5:
Write JUnit 5 tests using @ParameterizedTest for boundary condition testing. Group tests by category using @Nested. Include @DisplayName annotations. Cover: valid inputs, boundary values, null inputs, and illegal argument scenarios.What this does: Uses JUnit 5's parameterized test feature to cover multiple boundary values cleanly without duplicating test structure.
For async functions:
Write unit tests for this async JavaScript function. Make sure to: use async/await in tests, test both resolved and rejected promise paths, test timeout scenarios if applicable, and mock any external API calls with Jest mocks.What this does: Async functions have their own failure modes that synchronous tests won't catch — this prompt explicitly targets them.
⚡ Pro tip: After generating tests, ask: "Are there any test cases that are redundant with each other?" You'll often find the AI generated three tests that cover the same path. Trimming them keeps the suite fast and maintainable.
Next Steps
The shift Marcus made was from "coverage number" thinking to "risk coverage" thinking. Line coverage tells you what code the tests touch. It doesn't tell you whether the tests would catch the bug that's actually coming.
Prompts that ask for boundary conditions and invalid inputs get closer to risk coverage — which is what actually prevents incidents.
The long-term value of adversarial test prompts isn't just the tests they generate today. It's the thinking pattern they install. After running "boundary conditions + invalid inputs + assumptions" prompts for a few weeks, you start noticing edge cases during code writing — before the tests. That's the actual productivity gain: a week of prompting trains your intuition faster than a month of reading about it.
For teams doing test-driven development, the prompt works particularly well as a spec-first tool. Write the function signature, run the test generation prompt before writing the implementation, and use the assumption gaps to clarify the spec. You'll catch ambiguities in the design phase rather than the debugging phase.
⚡ Pro tip: For test suites that grow large, ask the AI to identify redundant tests: 'Review this test file and flag any tests that cover the same code path with the same inputs. Redundant tests slow CI without adding coverage value.' Pruning redundant tests keeps your suite fast — and forces clarity about what each test is actually verifying.
Build your test prompt template once, tune it for your stack and testing framework, then save it. PromptABCD is useful here — store the prompt with the framework-specific variations so your whole team runs the same quality standard, not just whoever remembered to use AI that sprint.
Testing Prompt Patterns by Framework
The core adversarial test prompt adapts well, but each testing framework has conventions worth including explicitly. Here are framework-specific additions that improve output quality significantly:
For Go testing:
Write Go tests using the table-driven test pattern. Each test case in the table should have a descriptive name field, input fields, and expected output. Include test cases for: happy paths, zero values, nil pointers, and boundary conditions at each threshold.What this does: Table-driven tests are idiomatic Go — this prompt produces tests that fit naturally into the codebase rather than looking like they came from a different language's style.
For Ruby RSpec:
Write RSpec tests using describe/context/it blocks. Use context blocks to group related scenarios (valid inputs, invalid inputs, edge cases). Use let for shared setup. Include examples for: expected behavior, error raising on invalid input, and boundary values.What this does: RSpec's nested describe/context structure makes boundary condition testing very readable — and this prompt enforces that structure rather than producing flat test files.
Mutation testing awareness:
After writing the tests, identify any tests that would still pass if this line of code were changed to its opposite: [paste specific line]. If a test wouldn't catch that mutation, suggest a stronger assertion that would.What this does: Introduces mutation testing thinking without requiring a mutation testing tool. It's a quick sanity check that your tests are actually verifying behavior rather than just running code without crashing.
⚡ Pro tip: Mutation-aware prompts are particularly valuable for financial and healthcare software, where a sign error or off-by-one in a boundary check can have serious consequences. Apply them to your highest-risk functions first. Then expand outward as the habit takes hold. The goal isn't perfect test coverage — it's tests that would have caught the bug that actually shipped.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
