Mock HTTP API Responses with Guzzle (PSR-18/PSR-7)

Use Case: HTTP Request to external API with PSR-18 Client

A very common use case in writing integrations with 3rd party services is to make an external request to an API - for example this could look something like this in TYPO3:

    public function __construct(
        RequestFactoryInterface $factory,
        ClientInterface $client
    ) {
        $this->factory = $factory;
        $this->client = $client;
    }

    // ...

    public function sendRequest(string $uri): array
    {
        $dataRequest = $this
            ->factory
            ->createRequest('GET', $uri);
        $response = $this->client->sendRequest($dataRequest);

        // example error handling
        if ($response->getStatusCode() >= 400) {
            $this->logger->warning('Something went wrong.');
            return [];
        }
        $responseBody = (string)$response->getBody();
        return (json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR));
    }

It would be nice if - instead of mocking the response, client, requestfactory etc - we could simply define a test client that delivers the expected response. However, I don't want to setup a test API server or anything like that. That's where Guzzle's mock handler comes in.

Using Guzzle's Mock Handler to mock client responses and verify requests

There are multiple different things I want to assert with my unit tests:

  • Is the correct request sent?
  • Is the response correctly interpreted?
  • Is the error handling reacting to errors in API responses?

To achieve this, I can use two helpers provided by Guzzle:

  • The MockHandler that allows me to mock response objects
  • The Guzzle History middleware that records all requests

The Setup: Guzzle MockHandler & History

I usually create a small helper method that allows me to get a test client:

    private function createClientWithHistory(array $responses, array &$historyContainer): Client
    {
        $handlerStack = \GuzzleHttp\HandlerStack::create(
            new \GuzzleHttp\Handler\MockHandler([
                    ...$responses,
            ])
        );
        $history = \GuzzleHttp\Middleware::history($historyContainer);
        $handlerStack->push($history);
        // implements \Psr\Http\Client\ClientInterface
        return new \GuzzleHttp\Client(['handler' => $handlerStack]);
    }

The method expects an array of responses that the client will later play back whenever a request is made and an (empty) array parameter that will be filled with the request history (by reference).

The Setup: Creating a client with response

In my test, I can then use my helper function to fetch a client with a mocked response object:

        $historyContainer = [];
        $client = $this->createClientWithHistory(
            [new \GuzzleHttp\Psr7\Response(200, [], file_get_contents(__DIR__ . '/Fixtures/200_data_response.json'))],
            $historyContainer
        );

The first parameter of response is the status code, followed by the headers array and the body. 

The Test: Assertions - Request and Response

Asserting the response of a test should nearly always be done through the core functionality of the unit we are currently testing:

// $client is the mock client we created in the last section
$service = new MyService(new RequestFactory(), $client);
$result = $service->getMyData();

self::assertSame(['my-expected-data'], $result)

With the help of the history container we could additionally verify that the request was as we expected:

// verify that only one request was sent
self::assertCount(1, $historyContainer);

// verify that the request called the correct URL - example for accessing request parameters
self::assertSame('https://example.com/my-api', (string)$historyContainer[0]['request']->getUri());

The Test: Testing Error Handling

A test testing an error condition from an external API could - in a simple case - look like this:

 $historyContainer = [];
 $client = $this->createClientWithHistory(
            // set up an error response
            [new Response(400)],
            $historyContainer
 );

 $service = new MyService(new RequestFactory(), $client);
 
 // we expect an error to be logged, so we need a logger to check
 $loggerProphecy = $this->prophesize(LoggerInterface::class);
 $service->setLogger($loggerProphecy->reveal());

 $result = $service->getData();

 self::assertSame([], $result);
 $loggerProphecy->warning('Something went wrong while fetching data.')->shouldHaveBeenCalled();

Full example

<?php

declare(strict_types = 1);

namespace Susanne\Examples\Tests\Unit\Services;


use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Susanne\Examples\Services\MyService;

class MyServiceTest extends UnitTestCase
{
    use ProphecyTrait;

  
    /**
     * @test
     * @covers \Susanne\Examples\Services\MyService::getData
     * @covers \Susanne\Examples\Services\MyService::__construct
     * @covers \Susanne\Examples\Services\MyService::sendAuthorizedRequest
     */
    public function getDataReturnsDataFromAPI(): void
    {
        $historyContainer = [];
        $client = $this->createClientWithHistory(
            [new Response(200, [], file_get_contents(__DIR__ . '/Fixtures/200_data_response.json'))],
            $historyContainer
        );

        $myService = new MyService(new RequestFactory(), $client);
        $result = $myService->getData();

        self::assertEquals($expected, $result);
        self::assertCount(1, $historyContainer);
        self::assertSame('https://example.com/api/', (string)$historyContainer[0]['request']->getUri());
    }

    private function createClientWithHistory(array $responses, array &$historyContainer): Client
    {
        $handlerStack = HandlerStack::create(
            new MockHandler([
                    ...$responses,
            ])
        );
        $history = Middleware::history($historyContainer);
        $handlerStack->push($history);
        return new Client(['handler' => $handlerStack]);
    }

}

More Info