In real life, your application would call real API to get results. In test environment, your application would pretend like it is calling the real API. This is how application test should work anyway. The example below calls an external API to return details about given postcode. This is what we're going to mock and return a different result instead. For mocking we're going to use TestDoubleBundle.


Setup


Install TestDoubleBundle


$ composer require "docteurklein/test-double-bundle":"1.0.0" --dev

Don't forget to enable it in AppKernel.php file. Use it in test only.


public function registerBundles()
{
if (in_array($this->getEnvironment(), ['test'], true)) {
$bundles[] = new DocteurKlein\TestDoubleBundle();
}
}

Parameters.yml


parameters:
....
....
postcodes_api: http://api.postcodes.io/postcodes/

PostcodeService


This service is called when a user calls http://app.dev/app_test.php/postcodes/REAL POSTCODE endpoint.


namespace AppBundle\Service;

use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Stream;
use Symfony\Component\HttpFoundation\Request;

class PostcodeService
{
private $client;
private $apiUri;

public function __construct(
ClientInterface $client,
$apiUri
) {
$this->client = $client;
$this->apiUri = $apiUri;
}

public function get($postcode)
{
return $this->getDetails($postcode);
}

private function getDetails($postcode)
{
$details = null;

try {
/** @var Response $response */
$response = $this->client->request(Request::METHOD_GET, $this->apiUri.$postcode);
/** @var Stream $body */
$body = $response->getBody();
$details = $body->getContents();
} catch (ClientException $e) {
}

return $details;
}
}

As you can see below, we're mocking GuzzleHttp\Client rather than whole service because Guzzle is the one that calls external API.


services:
app.service.postcode:
class: AppBundle\Service\PostcodeService
arguments:
- "@app.util.guzzle"
- "%postcodes_api%"

app.util.guzzle:
class: GuzzleHttp\Client
tags:
- { name: test_double }

Real life result


This is what we would get in real life for given postcode.


{
"status": 200,
"result": {
"postcode": "REAL POSTCODE",
"quality": 1,
"eastings": 123456,
"northings": 654321,
"country": "England",
"nhs_ha": "London",
"longitude": -0.87638765373434,
"latitude": 12.33905433211,
"parliamentary_constituency": "Whatever",
"european_electoral_region": "London",
"primary_care_trust": "Hello",
"region": "London",
"lsoa": "Hello 011U",
"msoa": "Hello 066",
"incode": "POSTCODE",
"outcode": "REAL",
"admin_district": "Hello",
"parish": "Hello, nice area",
"admin_county": null,
"admin_ward": "Hello Hill",
"ccg": "NHS Hello",
"nuts": "Hello",
"codes": {
"admin_district": "A0122222",
"admin_county": "B0122222",
"admin_ward": "C0122222",
"parish": "D0122222",
"ccg": "E0122222",
"nuts": "UK890"
}
}
}

Behat.yml


default:
extensions:
Behat\Symfony2Extension: ~
Behat\MinkExtension:
base_url: http://app.dev/app_test.php
sessions:
symfony2:
symfony2: ~
suites:
app:
type: symfony_bundle
bundle: AppBundle
mink_session: symfony2
contexts:
- AppBundle\Features\Context\FeatureContext:
- '%mink.base_url%'

FeatureContext.php


namespace AppBundle\Features\Context;

use Behat\Gherkin\Node\PyStringNode;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Symfony2Extension\Context\KernelAwareContext;
use GuzzleHttp\Psr7\Response;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\KernelInterface;

class FeatureContext extends MinkContext implements KernelAwareContext
{
private $baseUri;
/** @var KernelInterface */
private $kernel;
/** @var Crawler */
private $response;

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

public function setKernel(KernelInterface $kernel)
{
$this->kernel = $kernel;
}

/**
* @param string $method
* @param string $uri
*
* @When /^I send a "(GET|POST|PUT|PATCH)" request to "([^"]*)"$/
*/
public function iSendRequestTo($method, $uri)
{
/** @var Client $client */
$client = $this->getSession()->getDriver()->getClient();
$this->response = $client->request($method, $this->baseUri.$uri);
}

/**
* @param PyStringNode $stringNode
*
* @Then /^the response should contain json:$/
*/
public function theResponseShouldContainJson(PyStringNode $stringNode)
{
$expectedResponse = json_encode(json_decode($stringNode->getRaw()));

try {
$receivedResponse = json_encode(json_decode($this->response->text()));
if ($expectedResponse != $receivedResponse) {
throw new RuntimeException(sprintf('Response body contains: [%s]', $receivedResponse));
}
} catch (InvalidArgumentException $e) {
if (!$expectedResponse) {
throw new RuntimeException('Response body is empty.');
}
}
}

/**
* @param string $postcode
*
* @Given /^the Postcode API is available for "([^"]*)"$/
*/
public function thePostcodeApiIsAvailableFor($postcode)
{
$postcodesApi = $this->kernel->getContainer()->getParameter('postcodes_api').$postcode;

$response = new Response(200, [], '{"result":{"latitude":0.12345678,"longitude":-0.1234567}}');

$this->kernel
->getContainer()
->get('app.util.guzzle.prophecy')
->request(Request::METHOD_GET, $postcodesApi)
->willReturn($response);
}
}

Postcode.feature


As you can see below I'm using an unreal postcode and custom response to test. This wouldn't work if I didn't mock the API.


Feature: Getting postcode details.
In order to get postcode details
As a user
I should be able call call external API

Scenario: I get full result for existent postcode.
Given the Postcode API is available for "DUMMY POSTCODE"
When I send a "GET" request to "/postcodes/DUMMY POSTCODE"
Then the response status code should be 200
And the response should contain json:
"""
{"result":{"latitude":0.12345678,"longitude":-0.1234567}}
"""

Test


$ vendor/bin/behat --suite=app src/AppBundle/Features/Postcode.feature:13
Feature: Getting postcode details.
In order to get postcode details
As a user
I should be able call call external API

Scenario: I get full result for existent postcode.
Given the Postcode API is available for "DUMMY POSTCODE"
When I send a "GET" request to "/postcodes/DUMMY POSTCODE"
Then the response status code should be 200
And the response should contain json:
"""
{"result":{"latitude":0.12345678,"longitude":-0.1234567}}
"""

1 scenario (1 passed)
4 steps (4 passed)
0m0.47s (36.40Mb)