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.

You inherit a legacy PHP class with no tests, three years of undocumented edge cases baked into its logic, and a ticket asking you to refactor it safely. This is exactly the situation an AI prompt to write unit tests for legacy PHP earns its keep — not by replacing the work of understanding the code, but by turning a blank test file into a real starting suite in minutes instead of a full afternoon of manual test-writing.
Most developers who try this once get disappointed. They paste the class into an LLM, type "write unit tests for this," and get back a handful of trivial tests that check obvious happy paths while missing the actual edge cases the class was built to handle. The problem isn't the model — it's that the prompt never told it what "done" looks like for legacy code specifically.
Why "Write Tests for This" Alone Doesn't Work
A generic prompt has no way to know which behaviors in an untested class are load-bearing. Legacy PHP code frequently hides business rules in conditionals nobody remembers the reasoning for — a discount that only applies on Tuesdays, a null check that guards against a bug from two years ago, a fallback branch that only fires for one specific customer type. An LLM asked to "write tests" will test the code's structure. It won't go hunting for those hidden rules unless you explicitly ask it to.
The fix is the same principle that makes any AI prompt useful for a real engineering task: give it a checklist of what to look for, not an open-ended request.
What the Prompt Needs to Include
A prompt that produces a genuinely useful legacy-code test suite needs:
- The full class or method, including any related constants, enums, or config it reads — not a trimmed excerpt
- An explicit instruction to test current behavior, bugs and all — the goal before refactoring is a safety net, not a spec for how the code "should" work
- A request to identify branches and edge cases the tests must cover, listed out before the test code itself
- The testing framework and conventions you actually use (Pest, PHPUnit, a specific assertion style) so the output is usable without rewriting it
The Prompt Template
You are a senior PHP engineer writing a characterization test suite
for legacy code before a refactor. Test CURRENT behavior exactly as
written, including any bugs or odd edge cases — do not "fix" the
logic in the tests.
Framework: Pest (PHPUnit-based). Use `it()` and `expect()`.
Steps:
1. List every distinct branch, conditional, and edge case in the
code below.
2. For each one, write a test that pins down the current output.
3. Include at least one test per public method and one per
conditional branch.
CODE:
<paste class or method here>
Worked Example: Testing a Legacy Order Discount Method
Here's a small, realistic slice of legacy Laravel code with no tests:
class OrderDiscountCalculator
{
public function apply(Order $order): float
{
$total = $order->subtotal;
if ($order->customer->type === 'wholesale') {
$total *= 0.85;
} elseif ($order->items->count() >= 10 && now()->isTuesday()) {
$total *= 0.9;
}
if ($total < 0) {
$total = 0;
}
return round($total, 2);
}
}
Fed through the prompt above, the model first lists the branches it found — wholesale customers, the ten-item Tuesday discount, and the negative-total guard — then generates tests like this:
it('applies a 15% discount for wholesale customers', function () {
$order = makeOrder(subtotal: 200, customerType: 'wholesale');
expect((new OrderDiscountCalculator)->apply($order))->toBe(170.0);
});
it('applies a 10% discount for 10+ items on a Tuesday', function () {
Carbon::setTestNow(Carbon::parse('next tuesday'));
$order = makeOrder(subtotal: 100, itemCount: 10);
expect((new OrderDiscountCalculator)->apply($order))->toBe(90.0);
});
it('never returns a negative total', function () {
$order = makeOrder(subtotal: -50, customerType: 'retail');
expect((new OrderDiscountCalculator)->apply($order))->toBe(0.0);
});
Notice the second test — a Tuesday-only discount is exactly the kind of rule a quick manual read misses, and exactly what the explicit "list every branch first" instruction was designed to surface.
Common Mistakes
- Letting the model "clean up" the logic while writing tests — that turns your safety net into a test of behavior that never existed; always instruct it to test current behavior, not intended behavior.
- Testing only the method signature, not the branches — a class with three conditionals needs at least three tests, not one generic happy-path test.
- Skipping the branch-listing step — asking for tests and an inventory in one shot produces weaker coverage than asking for the inventory first and the tests second.
- Treating AI-generated tests as final — always run them against the actual code before trusting them; a hallucinated method signature or wrong assertion slips through more often on unfamiliar legacy code than on code you wrote yourself.
Best Practices for AI-Generated Tests in Legacy Code
- Run the generated tests immediately and confirm they pass against the existing code before touching anything
- Use the resulting suite as your refactor safety net — if a test breaks after a change, that's a real behavior change to review, not necessarily a bug
- Keep the framework and conventions section of the prompt saved as a template so every legacy file gets tests in a consistent style
- For a large legacy file, test one method or one class at a time rather than the whole file in one pass — smaller inputs produce more thorough branch coverage
Frequently Asked Questions
Will AI-generated tests catch every bug in legacy code? No — they capture current behavior, including existing bugs, which is exactly the point before a refactor. They're a safety net for detecting unintended changes, not a bug-finding tool on their own.
Should I ask the AI to fix bugs it notices while writing tests? Not in the same pass. Write characterization tests for current behavior first, commit them, and only then discuss potential fixes separately — otherwise your "before" baseline is already compromised.
Does this work for JavaScript or Python legacy code too? Yes — the same prompt structure applies to any language; just swap the framework and syntax conventions in the prompt for Jest, pytest, or whatever your stack uses.
How much of the legacy class should I paste into the prompt at once? As much as directly affects the method's behavior — the method itself plus anything it reads (constants, enums, related value objects) — but avoid pasting an entire multi-thousand-line file, since narrower input produces more thorough, more accurate branch coverage.
Key Takeaways
An AI prompt to write unit tests for legacy PHP earns real value the moment it's asked to test current behavior against an explicit list of branches and edge cases, instead of being told to just "write tests." Treat the output as a first draft safety net: run it against the existing code immediately, verify every generated assertion, and only then start refactoring with the confidence that a broken test means a real behavior change worth reviewing.


