Updating Symfony2 project to Behat 2.4

Behat and Symfony2 logo

Recent release of Behat 2.4 brings a lot of extensibility for a price of small backward compatibility breaks. Since I just went through an update of Behat in a Symfony2 project and there’s no guide on the subject yet, I thought it’s a good idea to share few tips.

Note: Read more on Behat 2.4 on @everzet’s blog: Behat 2.4: The most extendable testing framework.

Update: This blog post is now part of an official documenation for the Symfony2 Extension: Migrating from Behat 2.3 to 2.4.

There’s no BehatBundle nor MinkBundle anymore

Most important change for the Symfony users is that Behat integration is no longer done with bundles. Behat got its own extension system and there’s simply no need for bundles anymore.

So instead of the BehatBundle you’ll need to install the Symfony2Extension. MinkBundle was replaced by the MinkExtension and several drivers (i.e. MinkSeleniumDriver, MinkBrowserkitDriver).

Here’s an example composer.json snippet taken from my Symfony project using both selenium and browserkit drivers:

{
    "require": {
        "behat/behat":                  "*",
        "behat/symfony2-extension":     "*",
        "behat/mink-extension":         "*",
        "behat/mink-browserkit-driver": "*",
        "behat/mink-selenium-driver":   "*"
    }
}

Note: Naturally initialization of Behat and Mink bundles has to be removed from the AppKernel.

Accessing the Symfony kernel

If you’ve been extending BehatContext from BehatBundle to get access to the Symfony kernel you’ll need to alter your code and implement the KernelAwareInterface instead.

The Symfony kernel is injected automatically to every context implementing the KernelAwareInterface:

namespace Acme\Bundle\AcmeBundle\Features\Context;

use Behat\Behat\Context\BehatContext;
use Behat\Symfony2Extension\Context\KernelAwareInterface;
use Symfony\Component\HttpKernel\KernelInterface;

class AcmeContext extends BehatContext implements KernelAwareInterface
{
    /**
     * @var \Symfony\Component\HttpKernel\KernelInterface $kernel
     */
    private $kernel = null;

    /**
     * @param \Symfony\Component\HttpKernel\KernelInterface $kernel
     *
     * @return null
     */
    public function setKernel(KernelInterface $kernel)
    {
        $this->kernel = $kernel;
    }
}

Note: Read more on Symfony2Extension on github.

Accessing Mink session

It’s possible to inject Mink into the context just like it’s possible with the Symfony kernel. All you need to do is to implement the MinkAwareInterface.

Alternatively you can extend the RawMinkContext. It has an additional benefit of gaining access to several handy methods (like getSession(), assertSession(), getMinkParameter()).

namespace Acme\Bundle\AcmeBundle\Features\Context;

use Behat\MinkExtension\Context\RawMinkContext;

class AcmeContext extends RawMinkContext
{
    /**
     * @Given /^I go to (?:|the )homepage$/
     */
    public function iGoToHomepage()
    {
        $this->getSession()->visit('/');
    }
}

RawMinkContext can be safely extended multiple times since it doesn’t contain any step definitions (as opposed to MinkContext).

To take advantage of steps defined in the MinkContext you can simply add it as a subcontext:

namespace Acme\Bundle\AcmeBundle\Features\Context;

use Acme\Bundle\AcmeBundle\Features\Context\AcmeContext;
use Behat\Behat\Context\BehatContext;
use Behat\MinkExtension\Context\MinkContext;

class FeatureContext extends BehatContext
{
    public function __construct()
    {
        $this->useContext('acme', new AcmeContext());
        $this->useContext('mink', new MinkContext());
    }
}

Note: Read more on MinkExtension on github.

Behat configuration is now separated from Symfony

Instead of configuring Behat in Symfony you’ll need to create a new behat.yml file in the top level directory of your project:

default:
  formatter:
    name: progress
  extensions:
    Behat\Symfony2Extension\Extension:
      mink_driver: true
      kernel:
        env: test
        debug: true
    Behat\MinkExtension\Extension:
      base_url: 'http://www.acme.dev/app_test.php/'
      default_session: symfony2
      javascript_session: selenium
      selenium:
        host: 33.33.33.1
        port: 4444

You’ll have to remove your previous configuration (typically placed in app/config/config_test.yml). Otherwise dependency injection container will complain on unrecognised parameters.

Note: Read more on behat.yml in the configuration section of the official documentation.

There’s no Symfony command anymore

As the bundles disappeared and configuration has been separated, we have no Symfony specific command anymore. Behat is now run through its own script.

When using composer it’s good to specify the directory you want the commands to be installed in:

{
    "config": {
        "bin-dir": "bin"
    }
}

This way Behat will be accessible via:

./bin/behat

Including autoloader from composer

If you use composer you’ll need to make a small change to the app/autoload.php file. The require_once used to include the autoloader needs to be replaced with require:

$loader = require __DIR__.'/../vendor/autoload.php';

I didn’t dig into the details but I suspect Behat loads the autoloader first. Symfony tries to include it again and require_once returns false instead of the autoloader object.

Assertions

To use PHPUnit’s assertions you’ll need to include them first (I didn’t have to do it before):

require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';

It’s good for a start but later you’d probably prefer to use new WebAssert class. Assertions it provides are more suitable for web needs (you should get more meaningful error messages).

RawMinkContext provides a way to create WebAssert object with assertSession():

namespace Acme\Bundle\AcmeBundle\Features\Context;

use Behat\MinkExtension\Context\RawMinkContext;

class AcmeContext extends RawMinkContext
{
    /**
     * @Then /^I should see an error message$/
     */
    public function iShouldSeeAnErrorMessage()
    {
        $this->assertSession()->elementExists('css', '.error');
    }
}

Clearing Doctrine’s entity manager

When creating database entries with Doctrine in your contexts you might need to clear the entity manager before Symfony tries to retrieve any entities:

$entityManager->clear();

I needed to do it practically in every step which creates entities.

If you store objects in contexts (for future use in other steps) you’ll have to register them back in the entity manager before using (since you removed them with clear() call):

$entityManager->merge($this->page);

Running scenario suite for the whole project

At the moment there’s no way to run a whole project suite. I came up with a simple script solving that problem:

for feature_path in `find src/ -path '*/Features'`; do
    bundle=$(echo $feature_path | sed -e 's/^[^\/]\+\/\([^\/]\+\)\/Bundle\/\([^\/]\+\)\/.*/\1\2/');
    echo "Running suite for $bundle";
    ./bin/behat "@$bundle";
done

I also created another variation of it to be run on a CI server (generates reports): https://gist.github.com/2951321

Is it worth it?

I like to be up to date with libraries I use. Especially if I have to support the project for a long time.

With Behat 2.4 you’ll get a bit more than being up to date with the bug fixes. The way it’s been refactored enables you to better design your contexts. It’s much easier to make them decoupled from each other.

Jakub Zalas

Jakub Zalas

Agile PHP Developer, addicted to BDD and Symfony2