Robin van der Vleuten

Mock Guzzle responses in Symfony tests

Most web applications talk to at least one external service. Maybe you pull in tweets, send email, call a payment provider, or fetch data from an API. If you have functional tests around that code, you want them to be fast and predictable.

I had this problem with the latest-tweet bar on this site. The tweets were fetched with Guzzle, and Guzzle has built-in support for mocked responses. Here is how I used it in Symfony.

Guzzle explains response mocking in its docs. You add Guzzle\Plugin\Mock\MockPlugin as a subscriber to the client object. In Symfony, that client is probably registered as a service, so grab it from the container and add the plugin before running the request in your test:

php
<?php
use Guzzle\Http\Message\Response;
use Guzzle\Plugin\Mock\MockPlugin;
class HeaderControllerTest extends WebTestCase
{
public function testTweets()
{
$client = static::createClient();
$twitterClient = $client->getContainer()->get('rvdv_blog.guzzle.twitter_client');
$plugin = new MockPlugin();
$plugin->addResponse(__DIR__.'/twitter_200_response.txt');
$twitterClient->addSubscriber($plugin);
$crawler = $client->request('GET', '/_header_tweets');
$response = $client->getResponse();
$this->assertTrue($response->isSuccessful());
$this->assertTrue($response->isCacheable());
$this->assertEquals(3600, $response->getMaxAge());
$this->assertNotNull($response->getEtag());
$this->assertGreaterThan(0, $crawler->filter('li')->count());
}
}

The client comes from the container, and the mock plugin is added as a subscriber. The text file contains the raw response you would receive from the external service. For example:

bash
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2009 12:00:00 GMT
Connection: close
Server: AmazonS3
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">EU</LocationConstraint>

You can create multiple raw responses for different scenarios. Maybe you want to test a 500 response, a 401 response, or a malformed payload, and then check whether your application handles each case correctly.

To get those raw responses from the service, I used Charles. It is an HTTP proxy that sits between your local environment and the service. Turn Charles on and add these cURL options to your Guzzle client so you can inspect the traffic:

php
<?php
use Guzzle\Http\Client;
$client = new Client('https://api.twitter.com/{version}', array(
'version' => 'v1.1',
'request.options' => array(
// This should be the port you've configured Charles to listen for.
'proxy' => 'http://localhost:8888',
)
));

Once you have captured the responses, remove the proxy configuration and use the files in your tests.