PHPUnit: Mocking external data provider in functional tests

Mocking is a common thing in the world of automated testing, nothing to write home about. Nevertheless, I decided to document how to do it in Symfony with PHPUnit for future generations, or for when I forget and am too lazy to read the documentation (whichever comes first).

So I have an API application, it accepts REST requests and returns JSON responses, but some of the resources act as proxy to other external providers - how to test those?

Here is an example controller I have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

class MyController extends AbstractController {
    protected $externalDataLoader;

    public function __construct(ExternalDataLoader $externalDataLoader)
    {
        $this->externalDataLoader = $externalDataLoader;
    }

    /**
     * @Route ("my/resource/{id}")
     */
    public function myAction(int $id): Response
    {
        $data = $this->externalDataLoader->load('external/path', $id);

        // ... data is processed and formatted here.
        $result = $data;

        return $this->createResponse($result);
    }
}

and this ExternalDataLoader is registered as my_external_data_provider in the DI container.

So a simple action that takes an argument, makes a request to external resource and returns processed data. Now how to test this?

When it comes to REST APIs, I like to write functional (end-to-end) tests for two reasons:

  1. It’s not a tragedy when some piece of code is not covered with tests
  2. The actual functionality, as used from the outside, is tested

So here is a simple example on how to mock such an external data provider when writing a functional test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

public function myActionTest(): void
{
    $loaderMock = $this->getServiceMockBuilder('my_external_data_provider')
        ->setMethods(['load'])
        ->getMock();

    $loaderMock->expects($this->once())
        ->method('load')
        ->with('external/path', 1)
        ->willReturn(['something', 'something else']);

    if ($this->client === null) {
        $this->client = static::createClient(static::getKernelOptions());
    }

    $this->client->getContainer()->set('my_external_data_provider', $loaderMock);

    $this->client->request('GET', '/my/resource/1');
    $responseData = json_decode($this->client->getResponse()->getContent(), true);

    $this->assertSame([
        'data' => [
            'something',
            'something else',
        ],
        'jsonapi' => [
            'version' => '1.0',
        ]
    ], $responseData);
}

The first part mocks the externalDataLoader (my_external_data_provider service) and its load method with parameters and return value, and replaces it in the DI container.

The second part makes the actual request to /my/resource/1, and this is where the magic happens - mocked externalDataLoader is actually used, the parameters are checked and mocked return value is returned by the load method.

And that’s it, simple mocking.

Happy testing!