Search code examples
symfonyseleniumbehatmink

Optimising behat test suite


I have a test suite which has 20 feaure files and 100% MySQL CRUD operations are being carried out. It takes about 5 minutes to finish. If I did the test manually it would take about 7 minutes max. What I need to know is, what I need to do in order to optimise the whole process?

Note: ParallelRunnder is not supported for Behat 3 so it is out of scope for now!

If you're going to suggest to use Behat 3, then please help me modifying my composer.json & behat.yml files because when I do it myself and run bin/behat I get errors like:

`Behat\Symfony2Extension\Extension` extension file or class could not be located.
`Behat\MinkExtension\Extension` extension file or class could not be located. 
Unrecognized options "mink_driver" under "testwork.symfony2"
Unrecognized options "context, paths" under "testwork" 

As you can see below, I use certain-ish versions numbers, no star signs.

CURRENT composer.json:

"require": {
    "php": ">=5.3.3",
    "symfony/symfony": "2.5.4",
    "doctrine/orm": "~2.2,>=2.2.3",
    "doctrine/doctrine-bundle": "~1.2",
    "twig/extensions": "~1.0",
    "symfony/assetic-bundle": "~2.3",
    "symfony/swiftmailer-bundle": "~2.3",
    "symfony/monolog-bundle": "~2.4",
    "sensio/distribution-bundle": "~3.0",
    "sensio/framework-extra-bundle": "~3.0",
    "incenteev/composer-parameter-handler": "~2.0",
    "behat/behat": "2.5.3",
    "behat/behat-bundle": "1.0.0",
    "behat/symfony2-extension": "1.1.2",
    "behat/mink": "1.5.0",
    "behat/mink-extension": "~1.3",
    "behat/mink-selenium2-driver": "1.1.1",
    "behat/mink-goutte-driver": "1.0.9"
},

CURRENT behat.yml:

default:
    context:
        class: FeatureContext
        parameters:
            output_path: %behat.paths.base%/build/behat/output/
            screen_shot_path: %behat.paths.base%/build/behat/screenshot/
    extensions:
        Behat\Symfony2Extension\Extension:
            mink_driver: true
            kernel:
                env: test
                debug: true
        Behat\MinkExtension\Extension:
            base_url: 'http://symfony.local/app_test.php/'
            files_path: %behat.paths.base%/build/dummy/
            javascript_session: selenium2
            browser_name: firefox
            goutte: ~
            selenium2: ~
    paths:
        features: %behat.paths.base%/src
        bootstrap: %behat.paths.features%/Context

EDIT:

I have 20 feature files and one scenario in each. For the CRUD operations:

  • I have login method running in each scenario. Query runs against DB.
  • Some scenarios do INSERT, some to UPDATE and some do DELETE.
  • I'm using pdo_mysql as database_driver

A part of my FeatureContext file:

namespace Site\CommonBundle\Features\Context;

use Behat\MinkExtension\Context\MinkContext;
use Behat\Mink\Exception\UnsupportedDriverActionException;
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Symfony2Extension\Context\KernelAwareInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Process\Process;

/**
 * Class FeatureContext
 *
 * Parent to other FeatureContext files. It is used to avoid duplicated codes and all the
 * shared commons are kept here.
 *
 * @package Site\CommonBundle\Features
 */
class FeatureContext extends MinkContext implements KernelAwareInterface
{
    protected $kernel;
    protected $screenShotPath;
    protected $outputPath;

    /**
     * Parameter $parameters comes from behat.yml file.
     * @param array $parameters
     */
    public function __construct(array $parameters)
    {
        $this->outputPath = $parameters['output_path'];
        $this->screenShotPath = $parameters['screen_shot_path'];
    }

    /**
     * Helps to use doctrine and entity manager.
     * @param KernelInterface $kernelInterface Interface for getting Kernel.
     */
    public function setKernel(KernelInterface $kernelInterface)
    {
        $this->kernel = $kernelInterface;
    }

    /**
     * Without this, PhantomJs will fail if responsive design is in use.
     * @BeforeStep
     */
    public function resizeWindow()
    {
        $this->getSession()->resizeWindow(1440, 900, 'current');
    }

    /**
     * Take screen-shot when step fails. Works only with Selenium2Driver.
     *
     * @AfterStep
     * @param $event Current event.
     * @throws \Behat\Mink\Exception\UnsupportedDriverActionException
     */
    public function takeScreenshotAfterFailedStep($event)
    {
        if (4 === $event->getResult()) {
            $driver = $this->getSession()->getDriver();

            if (! ($driver instanceof Selenium2Driver)) {
                throw new UnsupportedDriverActionException(
                    'Taking screen-shots is not supported by %s, use Selenium2Driver instead.',
                    $driver
                );

                return;
            }

            if (! is_dir($this->screenShotPath)) {
                mkdir($this->screenShotPath, 0777, true);
            }

            $filename = sprintf(
                '%s_%s_%s.%s',
                $this->getMinkParameter('browser_name'),
                date('Ymd') . '-' . date('His'),
                uniqid('', true),
                'png'
            );

            file_put_contents($this->screenShotPath . '/' . $filename, $driver->getScreenshot());
        }
    }

    /**
     * @When /^I login as "([^"]*)"$/
     * @param $type User role type.
     */
    public function iLoginAs($type)
    {
        $container = $this->kernel->getContainer();
        $userData = $container->getParameter('dummy_user');

        $this->visit('/login');
        $this->fillField('username', $userData[$type]['username']);
        $this->fillField('password', $userData[$type]['password']);
        $this->pressButton('_submit');
    }
    .........
}

Solution

  • I don't know why I thought it takes 20 minutes to run, probably confused that with the number of features. 5 minutes is not bad at all. There are some basic things you can do that can help to speed it up.

    1. Logic inside your @BeforeStep – you probably can move it into @BeforeScenario or even into @BeforeFeature or even into @BeforeSuite – there's no need to do that so often.
    2. Very obvious, but just in case: Goutte driver is amazingly fast compared to others, the negative is that it doesn't support JS and you can't take screenshots. But I take it the Symfony app doesn't involve much of JS to do CRUD operations. So, double check everything that doesn't involve JS and use it with Goutte.
    3. iLoginAs is probably used in every step. You can update this to be much faster by manually creating a user session and setting back the cookie as a header, i.e., the same logic from your authentication method goes in the step definition and then you simply do $this->getSession()->getDriver()->setCookie(session_name(), session_id()); – on the next request your user is already authenticated.
    4. browser_name: firefox – god no… use chrome, it must be faster.
    5. Using HHVM for running Behat tests (the app still uses the old good PHP) will give a good increase in performance. Might be a pain installing it, especially on Mac.
    6. Finally, do migrate to Behat 3, that shouldn't be painful at all. They've taken a lot of knowledge from 2.x and used it to improve on it, including performance aspects. It's also much more flexible on the configuration, allows multiple contexts to keep things better organised and cleaner.