VGTech is a blog where the developers and devops of Norways most visited website share code and tricks of the trade… Read more



Are you brilliant? We're hiring. Read more

Generating code coverage of Behat tests

PHP

Yes, I know, it sounds silly, but bear with me.

The nature of acceptance tests is not really to tests units of code, but to assure that the behavior of your application meets a certain set of criteria (Behat Scenarios).

When your applications grow over time, code coverage can be a nice tool to help you pinpoint where you need to add more tests. In a perfect world tests are added while implementing new features so that your applications are always fully tested, but that isn’t always as easy as it sounds.

This post is a follow-up to Using PHP’s built-in web server in Behat tests that was published some months back, so if you haven’t read that one yet make sure to do so before continuing. If you are not familiar with Behat I suggest you have a look at the quick intro guide as well.

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:

Show code
<?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;

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:

Show code
private static $testSessionId;

/**
 * @BeforeSuite
 */
public static function setUp(Behat\Behat\Event\SuiteEvent $event) {
    self::$testSessionId = uniqid('behat-coverage-', true);   
}

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:

Show code
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);
}

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:

Show code
/**
 * @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']);                               
    }

    // ...
}

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.

Senior developer at VG. Coder of code, drinker/brewer of beer and listener of metal/punk/hc. @cogocogo | @BeerNorway | www.beernorway.com


2 comments

Leave your comment