When we write a client to integrate an API in our systems it is important to test it to be sure we can handle every possible response. Guzzle client provides a very simple way to mock external APIs responses: Guzzle Mock Handler. This tool provides a mock handler that can be used to fulfill HTTP requests with a response or exception by shifting return values off of a queue.
How does it work? Here’s an example provided by Guzzle documentation (https://docs.guzzlephp.org/en/stable/).
<?php use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Exception\RequestException; // Create a mock and queue two responses. $mock = new MockHandler([ new Response(200, ['X-Foo' => 'Bar']), new Response(202, ['Content-Length' => 0]), new RequestException("Error Communicating with Server", new Request('GET', 'test')) ]); $handler = HandlerStack::create($mock); $client = new Client(['handler' => $handler]); // The first request is intercepted with the first response. echo $client->request('GET', '/')->getStatusCode(); //> 200 // The second request is intercepted with the second response. echo $client->request('GET', '/')->getStatusCode(); //> 202
But how can we test our effective Symfony controller that has to handle all the responses? Let’s suppose we have a controller that returns a JsonResponse with a different message and status code based on what it gets from the APIs
<?php use Symfony\Component\HttpFoundation\JsonResponse; use MyNamespace\MyApiClient; public function apiControllerAction(MyApiClient $client) { $response = $client->getFoo(); if($response->getStatusCode()===200){ return new JsonResponse( [ 'status'=>'ok', 'message' => $response->getBody() ], 200 ); } if($response->getStatusCode()===404){ return new JsonResponse( [ 'status' => 'error', 'message' => 'foo not found' ], 404 ); } return new JsonResponse( [ 'status'=>'error', 'message' => 'something went wrong' ], 500 ); }
MyApiClient has a constructor that initialize the HTTP client and a method who retrieves “foo” from the API.
<?php namespace MyNamespace; use Psr\Http\Client\ClientInterface; class MyApiClient { public function __construct(ClientInterface $client) { $this->client = $client; } public function getFoo() { return $this->get('https://api-url/foo'); } }
Symfony will automatically inject MyApiClient in our controller, but to make sure it will inject ClientInterface in MyApiClient we have to modify services.yaml to look like this:
Psr\Http\Client\ClientInterface: '@psr18.client' psr18.client: class: GuzzleHttp\Client
This way we are telling Symfony to inject Guzzle HTTP Client in MyApiClient.
Now we have to create the functional test for our controller. The test will expect 3 different responses according to API responses. But before creating our test however, a preliminary step is needed. We have to create a class that extends Guzzle in order to easily manage the mock responses.
It will look like this:
<?php namespace Tests\Fake; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Client; /** * Class ClientFake */ class ClientFake extends Client { /** @var MockHandler */ protected $mockHandler; /** * ClientFake constructor. */ public function __construct() { $this->mockHandler = new MockHandler(); $handler = HandlerStack::create($this->mockHandler); parent::__construct(['handler' => $handler]); } /** * @param $responses */ public function appendResponse($responses): void { $this->mockHandler->append(...$responses); } }
This is not enough yet. We have to tell Symfony to use our ClientFake
instead of GuzzleHttp\Client when we are running our test. So we open our services_test.yml and we add following lines:
psr18.client: public: true class: Tests\Fake\ClientFake
When we are running our tests, Symfony will inject ClientFake in MyApiClient instead of real GuzzleClient. Are we ready to write our test now? Almost! We have to write a test case to initialize all the stuff we need.
<?php namespace Test; use Symfony\Bundle\FrameworkBundle\Client; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class MockHandlerTestCase extends WebTestCase { /** * @var Tests\Fake\ClientFake */ protected $clientInterface; /** @var Client */ protected $client; /** @var \Symfony\Component\DependencyInjection\ContainerInterface|null */ protected $Container; public function __construct(?string $name = null, array $data = [], string $dataName = '') { parent::__construct($name, $data, $dataName); $this->client = static::createClient(); $this->Container = $this->client->getContainer(); $this->clientInterface = $this->Container->get('psr18.client'); } protected function prepareMock($response) { $this->clientInterface->appendResponse([$response]); } }
And now we can finally write our tests.
Test 1 In the first one we simulate that APIs returns us a 200 code with a simple body.
<?php use Tests\MockHandlerTestCase; use GuzzleHttp\Psr7\Response as ClientResponse; use Symfony\Component\HttpFoundation\Response; class apiControllerTest extends MockHandlerTestCase { public function __construct($name = null, array $data = [], $dataName = '') { parent::__construct($name, $data, $dataName); } public function testOK(): void { //we set up our API mock response $response200 = new ClientResponse(Response::HTTP_OK, [], json_encode(['foo'=>'bar'])); $this->prepareMock($response200); //we navigate to our route $this->client->request('GET', '/routeToApiController'); //we expect a 200 (from controller - not from api) $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); //we put controller's response in a variable $result = \json_decode($this->client->getResponse()->getContent(), true); //we check that controller give us what we expect $this->assertEquals('ok', $result['status']); } }
Test 2 will test a 404 response.
<?php public function testNotFound(): void { //we set up our API mock response $response404 = new ClientResponse(Response::HTTP_NOT_FOUND); $this->prepareMock($response404); //we navigate to our route $this->client->request('GET', '/routeToApiController'); //we expect a 404 (from controller - not from api) $this->assertEquals(404, $this->client->getResponse()->getStatusCode()); //we put controller's response in a variable $result = \json_decode($this->client->getResponse()->getContent(), true); //we check that controller give us what we expect $this->assertEquals('error', $result['status']); }
In the last test we simulate a 500 answer.
<?php public function testGenericError(): void { //we set up our API mock response $response500 = new ClientResponse(Response::HTTP_INTERNAL_SERVER_ERROR); $this->prepareMock($response500); //we navigate to our route $this->client->request('GET', '/routeToApiController'); //we expect a 500 (from controller - not from api) $this->assertEquals(500, $this->client->getResponse()->getStatusCode()); //we put controller's response in a variable $result = \json_decode($this->client->getResponse()->getContent(), true); //we check that controller give us what we expect $this->assertEquals('error', $result['status']); $this->assertEquals('something went wrong', $result['message']); }
That’s all folks!
Motorcycle rider
American football player
DIY enthusiast
Web developer on free time