Hello everyone!

We have been investing plenty of personal time and energy for many years to share our knowledge with you all. However, we now need your help to keep this blog running. All you have to do is just click one of the adverts on the site, otherwise it will sadly be taken down due to hosting etc. costs. Thank you.

In this example we are going to test an external API with and without mocking Guzzle client. As we all know by now, external API calls must be mocked in testing environment. However, for demonstration purposes, I'll just show you how our test would look like if we didn't mock it.


Real API response


{
"status": 200,
"result": {
"postcode": "REAL POSTCODE",
"quality": 1,
"eastings": 1234,
"northings": 12345,
"country": "England",
"nhs_ha": "London",
"longitude": -0.123456789,
"latitude": 1.12345678,
"european_electoral_region": "London",
"primary_care_trust": "City",
"region": "London",
"lsoa": "City",
"msoa": "City 123",
"incode": "POSTCODE",
"outcode": "REAL",
"parliamentary_constituency": "City Central",
"admin_district": "City",
"parish": "City, unparished area",
"admin_county": null,
"admin_ward": "Westminister",
"ccg": "NHS City",
"nuts": "City",
"codes": {
"admin_district": "U1234567",
"admin_county": "U12345678",
"admin_ward": "U1234567",
"parish": "U123456",
"parliamentary_constituency": "U12345678",
"ccg": "U1234567",
"nuts": "UK1234"
}
}
}

Exception


namespace Application\Exception;

use Exception;

class PostcodesException extends Exception
{
public function __construct($message, $code = 400)
{
parent::__construct($message, $code);
}
}

Postcodes


This class calls external API to get UK postcode details and we will test it.


namespace Application\Util;

use Application\Exception\PostcodesException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Stream;

class Postcodes
{
private $client;
private $baseUri;

public function __construct(Client $client, $baseUri)
{
$this->client = $client;
$this->baseUri = $baseUri;
}

public function getData($postcode)
{
if (!$postcode) {
$this->throwException('Postcode is required.');
}

try {
/** @var Response $response */
$response = $this->client->request('GET', $this->baseUri.$postcode);
/** @var Stream $body */
$body = $response->getBody();

return $body->getContents();
} catch (ClientException $e) {
$this->throwException(sprintf('Failed to get postcode data for "%s".', $postcode));
}
}

private function throwException($message, $code = 400)
{
throw new PostcodesException($message, $code);
}
}

Without mocking


namespace tests\Application\Util;

use Application\Exception\PostcodesException;
use Application\Util\Postcodes;
use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;

class PostcodesTest extends TestCase
{
/** @var Postcodes */
private $postcodes;

protected function setUp()
{
$client = new Client();
$baseUri = 'http://api.postcodes.io/postcodes/';

$this->postcodes = new Postcodes($client, $baseUri);
}

protected function tearDown()
{
$this->postcodes = null;
}

public function testShouldThrowExceptionForEmptyPostcodeArgument()
{
$this->expectException(PostcodesException::class);
$this->expectExceptionMessage('Postcode is required.');
$this->expectExceptionCode(400);

$this->postcodes->getData('');
}

public function testShouldThrowExceptionForInvalidPostcodeArgument()
{
$postcode = 'INVALID';

$this->expectException(PostcodesException::class);
$this->expectExceptionMessage(sprintf('Failed to get postcode data for "%s".', $postcode));
$this->expectExceptionCode(400);

$this->postcodes->getData($postcode);
}

public function testShouldReturnPostcodeData()
{
$expected = file_get_contents(__DIR__.'/Mock/Postcodes/response-body.txt');

$result = $this->postcodes->getData('REAL POSTCODE');

$this->assertEquals($expected, $result);
}
}

#tests/Application/Util/Mock/Postcodes/response-body.txt

Content of this file is exactly the same as Real API response I added above.

Test


$ vendor/bin/phpunit --filter PostcodesTest tests/Application/Util/PostcodesTest.php
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

... 3 / 3 (100%)

Time: 97 ms, Memory: 4.25MB

OK (3 tests, 5 assertions)

With mocking


For more information, visit Testing Guzzle Clients.


namespace tests\Application\Util;

use Application\Exception\PostcodesException;
use Application\Util\Postcodes;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;

class PostcodesTest extends TestCase
{
public function testShouldThrowExceptionForEmptyPostcodeArgument()
{
$this->expectException(PostcodesException::class);
$this->expectExceptionMessage('Postcode is required.');
$this->expectExceptionCode(400);

$postcodes = $this->getPostcodes(400);

$postcodes->getData('');
}

public function testShouldThrowExceptionForInvalidPostcodeArgument()
{
$postcode = 'INVALID';

$this->expectException(PostcodesException::class);
$this->expectExceptionMessage(sprintf('Failed to get postcode data for "%s".', $postcode));
$this->expectExceptionCode(400);

$postcodes = $this->getPostcodes(400);

$postcodes->getData($postcode);
}

public function testShouldReturnPostcodeData()
{
$body = file_get_contents(__DIR__.'/Mock/Postcodes/response-body.txt');

$postcodes = $this->getPostcodes(200, $body);

$result = $postcodes->getData('XYZ XYZ');

$this->assertEquals($body, $result);
}

private function getPostcodes($status, $body = null)
{
$mock = new MockHandler([new Response($status, [], $body)]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);

return new Postcodes($client, 'http://mocked.postcodes.xyz/');
}
}

#tests/Application/Util/Mock/Postcodes/response-body.txt

{
"status": 200,
"result": {
"postcode": "XYZ XYZ",
"longitude": -0.000000,
"latitude": 1.111111
}
}

Test


$ vendor/bin/phpunit --filter PostcodesTest tests/Application/Util/PostcodesTest.php
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

... 3 / 3 (100%)

Time: 97 ms, Memory: 4.25MB

OK (3 tests, 5 assertions)