Take screenshot with Behat after failed step

Behat is a very popular tool for Behaviour Driven Develepment (BDD). It’s a very robust and complex tool – but one very handy feature seems to be missing. When step/scenario fails, you get a message indicating where it failed, but what would really help is a screenshot and/or the ability to see the page where it failed, so you can quickly detect where the problem was.

This can be solved effectively with the old PHPUnit Selenium RC extension, and similar functionality can also be replicated easily with the Behat AfterStep hook.

PHPUnit_Extensions_SeleniumTestCase has Capture a screenshot when a test fails feature. This could be added to PHPUnit_Extensions_Selenium2TestCase but for functional testing Behat became our weapon of choice and in Loft we replaced Selenium PHPUnit tests with Behat scenarios. Overall Gherkin as a language is much easier to read and tests can be written faster.

But how to add capture screenshot? The most simple and straightforward solution is to use the @AfterStep hook. Behat has a nice set of hooks where you can hook into the test process. In our case we are interested in the @AfterStep hook where the Behat\Behat\Event\StepEvent object, which contains an exception with a message where a step failed, is passed. This is the part which we need to modify.

For Selenium2Driver we can use $driver->getScreenshot() to obtain a screenshot. For other headless drivers we can have at least an HTML page where the test failed and obtain it by calling $this->getSession()->getPage().

The tricky bit is when and how to attach a message containing where the screenshot file or HTML page is stored so you can see it. Here it is necessary to use brute force. StepEvent::exception is where the exception with the message is stored. The problem is that exception is a private property. What we can do is to use Reflection to change the property to accessible and attach our message to the existing exception message.

Then the full AfterStep hook can look like this:

/**
 * @AfterStep
 */
public function dumpInfoAfterFailedStep(StepEvent $event)
{

    if ($event->getResult() === StepEvent::FAILED)
    {
        $session = $this->getSession();
        $page = $session->getPage();
        $driver = $session->getDriver();
        $message = '';

        $fileName = date('YmdHis') . '_' . uniqid();

        if (defined('HTML_DUMP_PATH'))
        {
            if (!file_exists(HTML_DUMP_PATH))
            {
                mkdir(HTML_DUMP_PATH);
            }

            $date = date('Y-m-d H:i:s');
            $url = $session->getCurrentUrl();
            $html = $page->getContent();

            $html = "<!-- HTML dump from behat  \nDate: $date  \nUrl:  $url  -->\n " . $html;

            $htmlCapturePath = HTML_DUMP_PATH . '/' . $fileName . '.html';
            file_put_contents($htmlCapturePath, $html);

            $message .= "\nHTML saved to: " . HTML_DUMP_PATH . "/". $fileName . ".html";
            $message .= "\nHTML available at: " . HTML_DUMP_URL . "/". $fileName . ".html";
        }

        if ($driver instanceof \Behat\Mink\Driver\Selenium2Driver && defined('SCREENSHOT_PATH'))
        {
            if (!file_exists(SCREENSHOT_PATH))
            {
                mkdir(SCREENSHOT_PATH);
            }

            $screenshot = $driver->getScreenshot();
            $screenshotFilePath = SCREENSHOT_PATH . '/' . $fileName . '.png';
            file_put_contents($screenshotFilePath, $screenshot);

            $message .= "\nScreenshot saved to: " . SCREENSHOT_PATH . "/". $fileName . ".png";
            $message .= "\nScreenshot available at: " . SCREENSHOT_URL . "/". $fileName . ".png";
        }

        $exception = $event->getException();

        $reflectionObject = new ReflectionObject($exception);
        $reflectionObjectProp = $reflectionObject->getProperty('message');
        $reflectionObjectProp->setAccessible(true);
        $reflectionObjectProp->setValue($exception, $exception->getMessage() . $message);
    }
}

Constants SCREENSHOT_PATH, SCREENSHOT_URL, HTML_DUMP_PATH, HTML_DUMP_URL could be stored in a config file and loaded in your FeatureContext.php file.

define('SCREENSHOT_PATH', '/var/tmp/screenshots');
define('SCREENSHOT_URL', 'http://localhost/screenshots');
define('HTML_DUMP_PATH', '/var/tmp/screenshots');
define('HTML_DUMP_URL', 'http://localhost/screenshots');

And this is the output:

Latest News & Insights

Say connected – get Loft updates straight to your inbox.