Mastering PHPUnit: Using Mocks and Stubs

May 30, 2024·
Gert de Pagter
Gert de Pagter
· 5 min read

This is part 4 of a series on PHPUnit testing, you can find part 3 here. In this post we’ll focus on the use of mocks. We’ll cover why we need them, the different types of mocks, and how to use them.

Why do we need mocks?

A mock is a stand in for a dependency that you dont want to (currently) test. The most commonly used example might be the database. In your unit tests, you don’t want to actually access the database. Instead you want to mock the usage of the database, so that you can test your logic without it. The two reasons for this are speed and isolation. To start with speed, unit tests need to be fast. If you unit test takes more then a few miliseconds it is slow. (Don’t take the startup time of PHPUnit into account for this.) In a bigger project you’ll likely have thousands if not tens of thousands of unit tests. If each of them would have to talk to the database, and then reset it afterwards, your test suite would take hours.

Another reason to have mocks is to test your code in isolation. Generally we want our unit tests to test one thing. So we dont’ really care what all the other classes are doing under the hood. Those have their own unit tests to cover that behaviour. Take the next bit of code for example. We fetch some data, and make sure the status is OK. In a unit test we dont want to use the HttpFetcher class, instead we’ll mock that, and only test the checking of status.

class BuildData
{

    public function __construct(
        private HttpFetcher $fetcher,
    ) {}


    public function build(): array
    {
        $data = $this-fetcher->fetch();

        if($data['status'] !== 'OK') {
            throw new Exception('Status was not OK.);
        }

        return $data;
    } 
}

Different types of mocks

I’ve been talking about mocks so far, but in PHP we can differentiate 2 types, which are Mocks and Stubs. The difference lies in what we do with them. On a stub we can configure behaviour. We can tell it what to return on a certain method call for example. On a mock we can also do this, but in addition to that we can also configure expecations. So we can make sure the method gets called twice, or with certain parameters.

If the ‘side effect’ of your method is what you are interested in, then you want to test that using mocks. Otherwise you should use a stub. For example, if you have a method which needs to make an API call with very specific parameters, then you can use a mock to test that that call was made with the right parameters. But if you have a method which needs to get some data from a dependency, then you should just use a stub.

In general i would always use a stub, unless its absolutely necesarry to use a mock. Which would only be if you really need to test that a certain call is made.

How to use mocks

Lets try to write a test for the BuildData class that we wrote a little earlier. Without mocks it would look something like this:

class BuildDataTest extends TestCase
{

    public function testItReturnsData(): void
    {
        $buildData = new BuildData(new HttpFetcher());

        $data = $buildData->build();

        $this->assertSame(['status' => 'OK'], $data);
    }
}

Now this test is gonna fail. The htpt server can’t be reaced, or maybe we need some more set up for that. But we don’t need to talk to the external server, if we use a stub for this. We can create a stub in PHPUnit with $this->createStub(<class_name>). So lets do that:

class BuildDataTest extends TestCase
{

    public function testItReturnsData(): void
    {
        $fetcher = $this->createStub(HttpFetcher::class);
        $fetcher->method('fetch')
            ->willReturn(['status' => 'OK']);

        $buildData = new BuildData($fetcher);

        $data = $buildData->build();

        $this->assertSame(['status' => 'OK'], $data);
    }
}

Now this test will pass. We use the willReturn to configure our stub to return the correct data. (in this case the ['status' => 'OK']) Since here it is not that important how many times something is called, or what it is called with, we dont need to use a mock, and we can use a stub. One example where the amount of times something is called, or with what it is called does become important is with cached calls. For example in the following adapter, we want to test that we only call our api once per id.

class CachedApi implements Api
{
    /** @var array<int, string> */
    private array $cache = [];

    public function __construct(
        private Api $innerApi,
    ) {}


    public function fetchApi(int $id): string
    {
        return $this->cache[$id] ??= $this->innerApi->fetchApi($id);
    } 
}

So for this class we could write the following tests, where we use createMock instead of createStub. We now also use the expects to configure how often something should be called. (You can also use $this->never() to configure if something should never be called). And in the first test method we use with, to configure what the method should be called with, since it is relatively important.

class BuildDataTest extends TestCase
{

    public function testItCallsInnerApiOnce(): void
    {
        $innerApi = $this->createMock(Api::class);
        $innerApi->expects($this->once())
            ->method('fetchApi')
            ->with(123)
            ->willReturn('foo');

        $cachedApi = new CachedApi($innerApi);

        $cachedApi->fetchApi(123);
        $cachedApi->fetchApi(123);
    }

    public function testItCallsInnerForEachId(): void
    {
        $innerApi = $this->createMock(Api::class);
        $innerApi->expects($this->exactly(2))
            ->method('fetchApi')
            ->willReturn('foo');

        $cachedApi = new CachedApi($innerApi);

        $cachedApi->fetchApi(123);
        $cachedApi->fetchApi(456);

        $cachedApi->fetchApi(456);
        $cachedApi->fetchApi(123);
    }
}

If you want to get notified of the next blog post, join the newsletter.