Originally published on phpdesignpatterns.dev — part of the PHP design patterns course. View this lesson on the original site.

One test per rule, one test per picker

One of Strategy's big practical wins is what it does to your tests.

Before — testing the calculator

In the Before code, every test had to construct the whole calculator and pass a customerType string:

<?php

it('gives loyalty plus a 15% discount up to 20 quid', function () {
    $calc = new DiscountCalculator();
    expect($calc->calculate('loyalty_plus', 20000))->toBe(2000);
});

That's not a bad test — but the test setup runs all the branches every time you call calculate. You're testing one rule via a function that knows about six. If one of the other branches breaks, this test won't catch it cleanly.

After — testing each strategy

<?php

it('caps the loyalty plus discount at 20 quid', function () {
    expect((new LoyaltyPlusDiscount())->calculateDiscount(20000))->toBe(2000);
});

it('gives 15% under the cap', function () {
    expect((new LoyaltyPlusDiscount())->calculateDiscount(10000))->toBe(1500);
});

The test now has one reason to fail: the loyalty-plus rule. The strategy is a one-line construction. You can run thousands of these and they're all fast.

Testing the picker

The selection logic — "guest gets NoDiscount, employee gets EmployeeDiscount" — is its own test:

<?php

it('picks the right strategy for each customer type', function () {
    $factory = new DiscountStrategyFactory();
    expect($factory->for('guest'))->toBeInstanceOf(NoDiscount::class);
    expect($factory->for('employee'))->toBeInstanceOf(EmployeeDiscount::class);
});

Two layers, two tests, no overlap.

Where integration fits

You still want one or two end-to-end tests that exercise the whole flow: customer type → strategy lookup → discount applied to cart total. But you don't need an end-to-end test per rule. The unit tests cover the rules; the integration test covers the wiring.

That ratio — many small unit tests for rules, a few integration tests for wiring — is one of Strategy's most underrated payoffs.

From PHP design patterns — Strategy: replacing branching with swappable behaviour