PHP 7: Maintaining and Upgrading Legacy Apps · Getting Legacy Code Under Test

Characterization Tests

Capture what the code does today before you change it.

Characterization tests record the current behavior of code — even if it seems wrong — so you notice when a refactor changes it. Call the code with representative inputs, capture outputs (return values, HTML, database rows), and assert on them. When a characterization test fails after a change, decide consciously whether the change was intended.

<?php
use PHPUnit\Framework\TestCase;

final class PriceCalculatorCharacterizationTest extends TestCase
{
    /** @dataProvider cases */
    public function testCurrentBehavior(array $cart, string $coupon, int $expected): void
    {
        // Values recorded by running the legacy code, not from a spec.
        $this->assertSame($expected, calculate_price($cart, $coupon));
    }

    public function cases(): array
    {
        return [
            'no coupon' => [[['price' => 1000, 'qty' => 2]], '', 2000],
            'ten percent' => [[['price' => 1000, 'qty' => 2]], 'TEN', 1800],
            'unknown coupon keeps price' => [[['price' => 500, 'qty' => 1]], 'BOGUS', 500],
        ];
    }
}

Common pitfalls

  • 'Fixing' surprising behavior inside a refactoring commit.
  • Too few inputs to cover important branches.
  • Characterization tests that depend on today's date or random data.

Key points

  • Record current behavior, not intended behavior
  • Cover important branches with varied inputs
  • Separate refactoring from behavior changes

Further reading: Michael Feathers: characterization testing

Practice questions

How is a characterization test different from a normal unit test?

Answer

It documents what the code currently does (discovered by running it) rather than what a specification says it should do. Its goal is to detect any behavior change during refactoring.

What should you do if you find a bug while writing characterization tests?

Answer

Record it (test documents current behavior, with a note), finish the refactor safely, then fix the bug in a separate change with a test asserting the correct behavior.

Open in the app to mark lessons complete, take notes and track your progress.