The first thing we will need to do is to add a router to PHP’s built in httpd. A router is a simple PHP script that will be executed before every request made against the httpd. In short, we want our router to do something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 | [code language="php"]<?php if (isset($_SERVER['HTTP_X_COLLECT_COVERAGE']) && isset($_SERVER['HTTP_X_TEST_SESSION_ID'])) { // Collect coverage from all files generated in this session, then exit } if (isset($_SERVER['HTTP_X_ENABLE_COVERAGE']) && isset($_SERVER['HTTP_X_TEST_SESSION_ID']) && extension_loaded('xdebug')) { // Enable XDebug's code coverage feature, and register a shutdown function that stops code coverage of the current // request, and stores the coverage of the current request to a tmp file } // Return false from the router to serve the requested file as is return false; [/code] |
The complete script (and all other code mentioned in this post) is available at the repository created for this post.
As you can see we can now enable code coverage of all the requests sent to the built in httpd by sending a couple of custom HTTP request headers called X-Enable-Coverage
and X-Test-Session-Id
. To be able to generate code coverage we also need the Xdebug extension to be loaded.
Now that we have a router that can generate code coverage on all requests it’s time to configure a web “browser” for our tests. There are many different solutions that can be used for this, but in this example I’ll use Guzzle. To configure the client to send the necessary headers we will need to add some logic to the FeatureContext
class used by Behat.
First we will generate a session ID for every test run (that will be used in the X-Test-Session-Id
header). This can be done in a @BeforeSuite
hook in the FeatureContext
class:
1 2 3 4 5 6 7 8 9 | [code language="php"] private static $testSessionId; /** * @BeforeSuite */ public static function setUp(Behat\Behat\Event\SuiteEvent $event) { self::$testSessionId = uniqid('behat-coverage-', true); }[/code] |
The setUp
method will be executed before the suite is executed (as the name of the hook implies).
Next we need to instantiate a Guzzle client that we can use for our tests. This can be done in the constructor of the FeatureContext
class for instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | [code language="php"] private $client; public function __construct(array $parameters) { $this->params = $parameters; $this->client = new Guzzle\Http\Client($this->params['url']); $defaultHeaders = array( 'X-Test-Session-Id' => self::$testSessionId, ); if ($this->params['enableCodeCoverage']) { $defaultHeaders['X-Enable-Coverage'] = 1; } $this->client->setDefaultHeaders($defaultHeaders); }[/code] |
The code above will make the client include the two headers on every request. The parameters are fetched from the .behat.yml
configuration file.
When the test suite is finished we need to collect all the generated code coverage and generate a report. This can be achieved by using some components from the PHP_CodeCoverage package in an @AfterSuite
hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | [code language="php"] /** * @AfterSuite */ public static function tearDown(Behat\Behat\Event\SuiteEvent $event) { $parameters = $event->getContextParameters(); if ($parameters['enableCodeCoverage']) { $client = new Guzzle\Http\Client($parameters['url']); $response = $client->get('/', array( 'X-Enable-Coverage' => 1, 'X-Test-Session-Id' => self::$testSessionId, 'X-Collect-Coverage' => 1, ))->send(); $data = unserialize((string) $response->getBody()); $filter = new PHP_CodeCoverage_Filter(); foreach ($parameters['whitelist'] as $dir) { $filter->addDirectoryToWhitelist($dir); } $coverage = new PHP_CodeCoverage(null, $filter); $coverage->append($data, 'behat-suite'); $report = new PHP_CodeCoverage_Report_HTML(); $report->process($coverage, $parameters['coveragePath']); } // ... }[/code] |
The tearDown
method will create an instance of a Guzzle client, set the correct headers and issue a request that will result in the aggregated code coverage of all requests made against the httpd during the test suite. The whitelist
parameter is used to specify which directories we want to appear in the report.
Now you should have a good understanding of how to add code coverage to your existing test suites, or a good starting point if you have yet to include a Behat suite for your project. Feel free to leave a comment or two if there is anything unclear, or if you have improvements to the code mentioned in this post. Remember to have a look at the repository on GitHub as well.