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
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
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.