- Published on
Mock HTTP API Responses with Guzzle (PSR-18/PSR-7)
- Authors
- Name
- Susanne Moog
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
- Guzzle testing documentation: https://docs.guzzlephp.org/en/stable/testing.html