Published on

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

Authors

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(
    \TYPO3\CMS\Core\Http\RequestFactoryInterface $factory,
    \Psr\Http\Client\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, request factory etc. – we could simply define a test client that delivers the expected response. However, I don't want to set up 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): \GuzzleHttp\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.

Testing error handling and returned data

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

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

With the help of the history container we can 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());

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();

        $expected = ['my-expected-data'];
        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