testing php applications

Test Doubles: Stubs And Mocks

A test double stands in for a collaborator. A stub returns controlled data so the code under test can proceed. A mock also verifies an interaction, such as whether an email gateway was called once.

Stub Data At Expensive Boundaries

Replace a remote API client when testing a service rule. Keep real database adapters for repository integration tests. A double is useful when the real collaborator would be slow, unreliable, or outside the test scope.

Mock Interactions Sparingly

Mock an interaction when the interaction is the behaviour: for example, a receipt email must be requested after payment succeeds. Avoid mocking every internal method, which makes refactoring painful.

PHP example
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

interface ExchangeRates
{
    public function gbpToEur(): float;
}

final class FixedRates implements ExchangeRates
{
    public function gbpToEur(): float
    {
        return 1.18;
    }
}

function eurosForPounds(int $pence, ExchangeRates $rates): int
{
    return (int) round($pence * $rates->gbpToEur());
}

final class PriceConverterTest extends TestCase
{
    public function testConvertsUsingTheProvidedRate(): void
    {
        $this->assertSame(1_180, eurosForPounds(1_000, new FixedRates()));
    }
}

FixedRates is a hand-written stub. PHPUnit can create doubles too, but a small explicit class is often easier to understand and reuse.

Common Mistakes

  • Mocking value objects and simple pure functions.
  • Asserting internal call order without a business reason.
  • Replacing all integrations with doubles and never testing real adapters.
  • Creating unrealistic stub responses.

What To Practise

  • Distinguish stubs from mocks.
  • Use doubles at appropriate boundaries.
  • Keep integration coverage for real adapters.

Practice

Practice: Stub A Currency Rate

Create a deterministic rate provider for a price-conversion service.

Requirements

  • Define a small collaborator interface.
  • Return a fixed rate from a stub.
  • Calculate one converted amount.
  • Explain why a live API is unsuitable for this unit test.
Show solution

The fixed collaborator makes the unit test deterministic.

PHP example
<?php

declare(strict_types=1);

interface Rates
{
    public function gbpToEur(): float;
}

final class FixedRates implements Rates
{
    public function gbpToEur(): float
    {
        return 1.18;
    }
}

function eurosForPounds(int $pence, Rates $rates): int
{
    return (int) round($pence * $rates->gbpToEur());
}

echo eurosForPounds(1_000, new FixedRates()) . PHP_EOL;

// Prints:
// 1180

Test the real HTTP adapter separately with an integration-style check or a fake HTTP server.