Skip to Content

Mock API requests in Codeception Acceptance tests

Posted on 5 mins read
Table of Contents

It’s pretty much guaranteed these days that your web application will make use of one or more 3rd party APIs. That implies you must also test how your code is handling them.

Because it’s a terrible idea to use the actual APIs in your tests, we need to mock their responses for these reasons:

  • You don’t want use your API credentials in a test environment.
  • Tests should run as fast as possible.
  • You need some predictability to verify the results.

PHP projects like mock-webserver, http-mock or Phiremock can help you with this.

I opted for mock-webserver because of its simplicity. It works very well for my intended purposes and keeps the mocking logic in my tests to an absolute minimum.

The complete example is also available on GitHub.

Sample application

Let’s write a PHP script that fetches quotes from quotesondesign.com. Store the following code in index.php. It will get a random quote, check if the status code was 200 OK and output the quote in HTML:

 1 2 3 4 5 6 7 8 910111213141516171819202122232425262728293031323334353637
<?php
$config = require('config.php');

$ch = curl_init();

$options = [
  CURLOPT_URL            => $config['API_URL'] . '?filter[orderby]=rand&filter[posts_per_page]=1',
  CURLOPT_RETURNTRANSFER => true
];

curl_setopt_array($ch, $options);
$result = curl_exec($ch);
$code   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($code == 200)
{
  $quotes = json_decode($result);
  $quote  = array_shift($quotes);

  $status = 200;
  $output = <<<EOF
  <h3>Quote of the day</h3>
  <blockquote>
     $quote->content
     <p>-- <em>$quote->title</em></p>
  </blockquote>
EOF;
}
else
{
  $output = sprintf("API returned status %d", $code);
  $status = 500;
}

http_response_code($status);
echo $output;

The API URL is defined in the config.php file:

<?php
return [
	'API_URL' => 'http://quotesondesign.com/wp-json/posts'
];

Start PHP’s standalone web server:

php -S localhost:8080

and browse to http://localhost:8080 to see it in action. Example output:

Output of sample application

Create Codeception test

Now we want to set up Codeception to run an Acceptance test against our code. Run the following commands to set it up in your project:

composer require --dev "codeception/codeception"
codecept bootstrap

Now generate a new Acceptance test:

codecept generate:cest acceptance Example

Configure the Acceptance test suite in tests/acceptance.suite.yml. To run tests on the standalone server we started earlier, your config file should look like this:

actor: AcceptanceTester
modules:
enabled:
- PhpBrowser:
	url: http://localhost:8080/
	- HelperAcceptance

Open the tests/acceptance/ExampleCest.php file and add the checkQuote test to it:

<?php
    public function checkQuote(AcceptanceTester $I)
    {
        $I->amOnPage('/');
        $I->seeResponseCodeIs(200);
        $I->see('Quote of the day', 'h3');
        $I->seeElement('blockquote');
    }

This test will go to http://localhost:8080, makes sure the status code is 200 and look for the correct HTML output.

Run Codeception to verify that the acceptance test passes:

codecept run acceptance --steps

Add Mock HTTP server

There are a couple of problems with this test. Firstly, it relies on the external API to be available at all times. Secondly, we can’t test how our code handles a potential outage of the API service. Finally, it’s impossible to test for specific results.

To solve this we’ll add donatj/mock-webserver to our project. This allows us to recreate those API responses locally and have full control over its output.

Add the package to your project:

composer require --dev "donatj/mock-webserver"

With this package we can easily create mock HTTP responses. For example, let’s start a server that will return a json object for any request to http://localhost:8001/foo:

<?php  
use donatj\MockWebServer\MockWebServer;
use donatj\MockWebServer\Response;

require_once 'vendor/autoload.php';

$server = new MockWebServer(8001);
$server->start();
$server->setResponseOfPath('/foo', new Response(json_encode(['foo' => 'bar'])));

echo "/foo returns " . file_get_contents('http://localhost:8001/foo');

Run php test.php to receive the following output:

/foo returns {“foo”:“bar”}

Couldn’t get any easier!

Integrate Mock Webserver into Codeception

All that’s left to do is tying everything together in the acceptance test.

First import the package classes. Add these use statements to the top of the tests/acceptance/ExampleCest.php file:

<?php
use donatj\MockWebServer\MockWebServer;
use donatj\MockWebServer\Response;

Now create a server instance in the _before() method, and shut it down in _after():

<?php

class ExampleCest  
{
    private static $__mock_webserver;
    
    public function _before(AcceptanceTester $I)
    {
        $server = new MockWebServer(8001);
        $server->start();
    
        self::$__mock_webserver = $server;
    }
    
    public function _after(AcceptanceTester $I)
    {
        self::$__mock_webserver->stop();
    }
    

We now have our mock server available to us in every test. Let’s update our existing checkQuote() method to add a valid response for the /wp-json/posts path, and verify that the quote is actually in the HTML output:

<?php

    public function checkQuote(AcceptanceTester $I)
    {
        // Set up a valid response for the /wp-json/posts path:
        self::$__mock_webserver->setResponseOfPath(
            '/wp-json/posts',
            new Response('[{"ID":2224,"title":"Georgia O\u2019Keeffe","content":"<p>To see takes time.<\/p>\n","link":"https:\/\/quotesondesign.com\/georgia-okeeffe-2\/"}]')
        );
    
        $I->amOnPage('/');
        $I->seeResponseCodeIs(200);
        $I->see('Quote of the day', 'h3');
        $I->seeElement('blockquote');
        // Make sure that the output is using the quote returned from the mock server:
        $I->see('To see takes time', 'p');
    }

We can now update API_URL in our config file on the test environment to point to the mock server:

<?php
return [
	'API_URL' => 'http://localhost:8001/wp-json/posts'
];

Finally, we add a test to check that our error handling works. In our example, the script returns a 500 status code and the message "API returned status x" if an error occurs.

We can mock this by returning a 500 error code and an empty response for any request to the /wp-json/posts path:

<?php

  public function checkQuoteErrorHandling(AcceptanceTester $I)
  {
    self::$__mock_webserver->setResponseOfPath(
      '/wp-json/posts',
      new Response('', [], 500)
    );

    $I->amOnPage('/');
    $I->seeResponseCodeIs(500);
    $I->see('API returned status', 'p');
  }

Re-run the Codeception tests and make sure they pass:

codecept run acceptance --steps

All done!

For better reusability, I suggest to move the instantiation of the Mock Webserver into a Codeception extension. I’ll handle that in another blog post sometime.