← Back to blog

Test doubles in PHP - Dummies and Stubs

| PHP

A dummy is an object that replaces all the original methods with a dummy implementation that returns null (without calling the original method).

You can create a dummy object using the createMock method in PHPUnit:

use GuzzleHttp\Client;

$client = $this->createMock(Client::class);

Inspecting the dummy object using dd($client);:

Mock_Client_8cd31d5b {#129
  -config: null
  -__phpunit_originalObject: null
  -__phpunit_returnValueGeneration: true
  -__phpunit_invocationMocker: null
}

The createMock method creates a random class -- in this case it was named Mock_Client_8cd31d5b. This class extends GuzzleHttp\Client:

dd(
    class_parents($this->createMock(Client::class)),
);

Output:

array:1 [
  "GuzzleHttp\Client" => "GuzzleHttp\Client"
]

It also overrides all the original methods so they return null:

$client = $this->createMock(Client::class);

// This line returns null
dd(
    $client->get('https://example.com')->getBody()
);

Note that createMock replaces all objects recursively. The get method will return a new dummy implementation of Psr\Http\Message\ResponseInterface.

Now that you know what dummies are, here is how to use them.

Make the following changes to the CurrencyConvertor class:

<?php

namespace App;

use Exception;
use GuzzleHttp\Client;

class CurrencyConversion
{
    public function __construct(private Client $client)
    {
    }

    public function convert(string $from, string $to, int|float $amount): float
    {
        $query = http_build_query([
            'from' => $from,
            'to' => $to,
            'amount' => $amount,
        ]);

        $req = $this
            ->client
            ->request('GET', 'https://api.exchangerate.host/convert?'.$query);

        if ($req->getStatusCode() !== 200) {
            throw new Exception('Could not convert!');
        }

        $res = json_decode($req->getBody()->getContents());

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception('Invalid response!');
        }

        return $this->formatNumber($res->result);
    }

    public function formatNumber(float $amount, int $decimals = 2): float
    {
        return (float) number_format($amount, $decimals);
    }
}

The number_format logic is now in its own method.

Here is how to test the formatNumber method:

public function test_format_number()
{
    $client = $this->createMock(Client::class);
    $currencyConvertor = new CurrencyConversion($client);

    $this->assertEqualsWithDelta(
        10.99,
        $currencyConvertor->formatNumber(10.999)
    , 0.1);
}

CurrencyConvertor::__construct requires an instance of GuzzleHttp\Client but does not use it unless you call the convert method.

Stubs

The practice of replacing an object with a test double that (optionally) returns configured return values is referred to as stubbing. PHPUnit.

In the previous post, we mocked different objects, for example the response object:

$response = $this->createMock(ResponseInterface::class);

$response
    ->method('getStatusCode')
    ->willReturn(200);

This is stubbing because you are configuring an object that returns a predefined value when called.

Conclusion

The difference between Dummies and Stubs lies in the return value. A Dummy always returns null, whereas a Stub returns a predefined value.

PHPUnit supports a number of stubbing methods such as willReturnArgument, willReturnSelf, willThrowException. Consult the documentation to learn more about them.

Summary

  • Dummy -- a test double that replaces all original methods with implementations returning null, created via createMock().
  • Stub -- a test double configured to return specific predefined values when its methods are called.
  • Key difference -- Dummies return null by default; Stubs return values you explicitly configure.
  • createMock -- recursively replaces all objects, so chained method calls on a dummy also return dummy implementations.
Share