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

Seams and Breaking Dependencies

Create seams so hard-to-test code can be isolated.

A seam is a place where you can change behavior without editing the code under test — for example by passing an object instead of creating it inside, or by overriding a method in a test subclass. Techniques from Working Effectively with Legacy Code: parameterize constructor/method, extract and override call, wrap static/global calls in an injectable adapter, and extract interface.

<?php
// Before: impossible to test without sending real email
class OrderProcessor {
    public function complete($orderId) {
        // ...
        mail($customerEmail, 'Thanks!', $body);
    }
}

// After: seam via an injectable mailer, defaulting to old behavior
interface Mailer { public function send(string $to, string $subject, string $body): void; }

final class NativeMailer implements Mailer {
    public function send(string $to, string $subject, string $body): void { mail($to, $subject, $body); }
}

class OrderProcessor {
    private $mailer;
    public function __construct(Mailer $mailer = null) { $this->mailer = $mailer ?? new NativeMailer(); }
    public function complete(int $orderId): void {
        // ...
        $this->mailer->send($customerEmail, 'Thanks!', $body);
    }
}

Common pitfalls

  • Large refactors before having any tests.
  • Default-argument seams left forever instead of proper wiring later.
  • Mocking everything instead of introducing a few meaningful seams.

Key points

  • Seams let tests substitute behavior
  • Wrap globals/static calls in adapters
  • Keep old call sites working while introducing seams

Further reading: Working Effectively with Legacy Code

Practice questions

What is 'extract and override call'?

Answer

Move a problematic call (e.g., to a web service) into its own protected method, then in a test subclass override that method to return a canned value.

Why give a new constructor parameter a default value during legacy refactoring?

Answer

So existing callers keep working unchanged while tests can inject a fake; later you can wire the dependency properly and remove the default.

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