AI Prompt for Unit Tests on Legacy Code
No tests. No comments. A 90-line method that touches three different services and somehow still works in production. Every PHP and JS codebase I've inherited from a client has at least one function like this, and writing tests for it by hand is usually a half-day job nobody budgets for. This is where an AI prompt for unit tests actually earns its keep — not as a toy, but as the thing that gets a genuinely untested function covered in twenty minutes instead of four hours.
I want to walk through the exact prompt I use, why most of the generic "write me tests" prompts people share online produce garbage, and what the output actually needs to look like before it's safe to commit.
The function that started this
Here's a simplified version of a real one I dealt with on a client's Laravel order system — calculating a shipping discount based on cart total, customer tier, and an active promo code that could be null, expired, or stacked with the tier discount.
function calculateShippingDiscount(float $cartTotal, string $tier, ?Promo $promo): float
{
$discount = match ($tier) {
'gold' => 0.15,
'silver' => 0.08,
default => 0.0,
};
if ($promo && $promo->isActive() && !$promo->isExpired()) {
$discount = $promo->stacksWithTier
? $discount + $promo->rate
: max($discount, $promo->rate);
}
return round($cartTotal * min($discount, 0.5), 2);
}
No tests existed for this. Four branches, a nullable argument, a cap at 50%, and rounding — exactly the kind of function where "looks fine" in a code review and "actually correct" are two different things.
Why the naive prompt fails
"Write unit tests for this function" produces tests that call the function once with obvious inputs and assert it doesn't throw. I've watched this happen on three different projects now. The AI isn't being lazy — it genuinely doesn't know which branches matter unless you tell it what the function is supposed to guarantee, not just what it does.
The prompt that actually works
I give the model the function, the framework, and — this is the part everyone skips — an explicit list of the behaviors that must be verified, phrased as guarantees rather than descriptions:
Write Pest tests for this PHP function. Framework: Laravel 11, Pest 3.
Use a real Promo model instance in tests, not a loose stdClass.
Guarantee each of these with its own test case:
1. Gold tier with no promo returns exactly 15% off, rounded to 2 decimals.
2. Silver tier with an expired promo ignores the promo entirely.
3. A non-stacking promo takes the higher of the two discounts, not both.
4. A stacking promo adds both discounts together.
5. Combined discount never exceeds 50%, even if tier + promo would sum higher.
6. Unknown/empty tier string falls back to 0% base discount.
Name each test after the behavior it guarantees, not the input it uses.
That last line matters more than it sounds like it should. test_gold_tier() tells you nothing when it fails six months from now; test_expired_promo_is_ignored_even_if_rate_is_higher() tells you exactly what broke.
What the output looked like
it('never exceeds a 50% combined discount cap', function () {
$promo = Promo::factory()->create([
'rate' => 0.6,
'active' => true,
'stacksWithTier' => true,
]);
$result = calculateShippingDiscount(200.0, 'gold', $promo);
expect($result)->toBe(100.0); // 50% of 200, not 75%
});
That one test caught a real bug on my first pass — my original implementation capped the rate at 0.5 but applied it before the tier discount was added, so a 0.6 promo rate plus a 0.15 tier discount briefly produced a 65% effective discount before the cap kicked in on the wrong value. The test failed, which is the whole point of writing it before shipping.
Where I'd push back on the common advice
Most "AI for testing" posts tell you to paste the function and accept whatever comes back, maybe with a quick skim. I wouldn't. The failure mode isn't obviously bad tests — it's tests that pass, look thorough, and quietly assert the wrong thing because the model guessed at behavior instead of being told it. A test that asserts expect($result)->toBeFloat() instead of checking the actual value will stay green forever while telling you nothing. Read every assertion against the guarantee it's supposed to prove, not just whether the test suite goes green.
I also skip mocking anything that's cheap to construct for real, like the Promo model above. AI-generated mocks have a habit of mocking away the exact interaction a bug would live in.
Frequently Asked Questions
Does this replace writing tests myself? No — it replaces the part where you stare at an untested function trying to remember every edge case. I still read and often rewrite a couple of the generated assertions, especially around rounding and boundary values like the 50% cap here.
What if the function doesn't have a spec or clear business rules? Then the prompt is where you figure that out. Writing the guarantee list forces you to actually define correct behavior, often surfacing the fact that nobody agreed on what "stacking" should mean before the code was written.
Unit tests generated this way are only as good as the guarantees you hand over before the prompt runs — write those down first, read every assertion against them, and treat a clean test run as a starting point for review, not proof the function is correct.
Related posts
AI Prompt to Write Unit Tests for Legacy PHP
Learn how to write an AI prompt that generates real unit tests for legacy PHP code, including the hidden edge cases a generic prompt misses.
5 AI Prompts to Refactor Messy Code
5 AI prompts that turn vague refactor requests into mergeable code — name the exact smell, protect behavior, and lock in tests before you touch anything.
AI Code Review Prompt for Laravel PRs
A structured AI code review prompt that actually catches Laravel-specific bugs like mass assignment and N+1 queries, instead of generic AI praise.
AI Prompts for Better Git Commit Messages
Most AI commit message tools just summarize the diff. Here's the AI prompt and git hook I use that actually captures the real reason behind a change.
0 Comments
No comments yet — be the first to share your thoughts.