Implementing functional tests in Symfony2 with the bundled WebTestCase and DOMCrawler is quite easy and quick, letting you cover all of your controllers without making a single real http call to the symfony application. Testing your application offline is a great plus, but in the real world this cannot be taken as granted, because we often have to deal with external services (e.g. 3rd party APIs) which need real http calls. We can accept the idea of testing those services using their real API, but we will have to deal with some annoying problems: we have to be online for running tests; we have to deal with the garbage test data we eventually produce; we can't easily simulate a service down; we will end up with a slow test suite if we make a lot of remote calls; once in a while we will have red tests because of a 3rd party service down, and we know it will happen just before that critical deployment, making us loosing faith in our test suite.

The other way around, we can choose to mock the interaction with external services in order to let our tests run offline, simulate every possible service status we need,  drastically improving performances.

Consider this scenario: you have a service defined in your DIC through which all the calls to the external API pass. Let's keep it simple, we will define a simple get method:

<?php
 
namespace SpaghettiDashboardBundleRemoteApi;
 
use BuzzBrowser;
 
class Client
{
    private $httpClient;
 
    public function __construct(Browser $httpClient)
    {
        $this->httpClient = $httpClient;
    }
 
    public function get($uri)
    {
        $response = $this->httpClient->get($uri);
 
        return json_decode($response->getContent(), true);
    }
}

and expose the Client as a service:

services:
    spaghetti_dashboard.remote_api_client:
        class: SpaghettiDashboardBundleRemoteApiClient
        arguments: [@buzz]

We use kriswallsmith/buzz as http client, and use our service in a controller:

<?php
 
namespace SpaghettiDashboardBundleController;
 
use SymfonyBundleFrameworkBundleControllerController;
 
class DefaultController extends Controller
{
    public function showExternalItemsAction()
    {
        $client = $this->get("spaghetti_dashboard.remote_api_client");
        $items = $client->get("https://api.spaghetti.io/items.json");
 
        return $this->render(
            "SpaghettiDashboardBundle:Default:externalItems.html.twig",
            compact("items")
        );
    }
}

This is our test:

<?php
 
namespace SpaghettiDashboardBundleTestsController;
 
use SymfonyBundleFrameworkBundleTestWebTestCase;
use SymfonyComponentHttpFoundationResponse;
 
class DefaultControllerTest extends WebTestCase
{
    public function testExternalItems()
    {
        $client = static::createClient();
 
        $crawler = $client->request("GET", "/externalItems");
        $this->assertEquals(Response::HTTP_OK, $client->getResponse()->getStatusCode());
        $this->assertCount(3, $crawler->filter("#items li"));
    }
}

What we need to do now is substituting the real service inside the container with a mock. We will use PHPunit mock, but you can use the mocking framework you prefer. We extend the WebTestCase createClient function and overwrite the mocked service inside the container

Now we can start using expectations on the mock to make its get method return what we need in each test:

<?php
 
namespace SpaghettiDashboardBundleTestsController;
 
use SymfonyBundleFrameworkBundleTestWebTestCase;
use SymfonyComponentHttpFoundationResponse;
 
class DefaultControllerTest extends WebTestCase
{
    private static $buzzMock;
 
    protected static function createClient(array $options = array(), array $server = array())
    {
        $client = parent::createClient($options, $server);
 
        $container = self::$kernel->getContainer();
        $container->set("buzz", self::$buzzMock);
 
        return $client;
    }
 
    public function testExternalItems()
    {
        self::$buzzMock = $this->getMockBuilder("Buzz\Browser")
            ->disableOriginalConstructor()
            ->getMock();
 
        $mockedItems = array(
            array("name" => "item1"),
            array("name" => "item2"),
            array("name" => "item3")
        );
 
        $response = $this->getMockBuilder("Buzz\Message\Response")
            ->disableOriginalConstructor()
            ->getMock();
 
        self::$buzzMock
            ->expects($this->once())
            ->method("get")
            ->will($this->returnValue($response));
 
        $response
            ->expects($this->once())
            ->method("getContent")
            ->will($this->returnValue(json_encode($mockedItems)));
 
        $client = static::createClient();
 
        $crawler = $client->request("GET", "/externalItems");
        $this->assertEquals(Response::HTTP_OK, $client->getResponse()->getStatusCode());
        $this->assertCount(3, $crawler->filter("#items li"));
    }
}

Note: this method will allow you to replace the service just for one WebTestCase client call. If you want to make a second call inside the same test, you will have to call self::createClient() again, otherwise the container will be rebuilt.

Another possible way to mock the interaction with the external service is defining a Stub that implements the same interface of the real service, and substitute the class value inside the config_test.yml file. This could work too, but if you want that the same methods return different data in different calls, it becomes a bit difficult to implement

Giorgio Cefaro

Freelance Software Engineer , Website , Git home page , @giorrrgio , Linkedin profile
Software Engineer, PHP specialist, coach, public speaker, open source developer and enthusiast, works in the IT field since 1999. TDD addicted, DDD worshipper

All articles by Giorgio Cefaro

Comments

comments powered by Disqus

cloudparty

Follow Us