Kamil Kokot

Tame Behat with the Brand New Symfony Extension

February 11, 20197 minutes read 0 comments Edit on GitHub

Over two years after the first release of this Friends of Behat’s Symfony Extension, I’m happy to announce the availability of the second major version and write more about it.

It is the already battle-tested solution with Sylius on 1300+ scenarios containing 14000+ steps, getting traction in projects like Akeneo or Monofony.

The mission behind it is to simplify the process of implementing Behat contexts so that you can focus on the communication with stakeholders and writing down business features instead of figuring out low-level testing infrastructure details.

TL;DR

  • Support for Symfony’s autowiring and autoconfiguration
  • Symfony Flex contrib recipe to make installation easier
  • Zero-configuration for most Symfony 3 and 4 based apps
  • Unified application configuration and dotenv file handling (similar to the PHPUnit one)
  • Integration with Mink, available out of the box
  • Simplified usage, reduced the number of extensions from three to one
  • Improved developer experience with more descriptive exceptions and more predictable overall behaviour

If you want to try it first, dive straight into the documentation.

I am working on an entry-level tutorial for SymfonyExtension v2, make sure to follow me on Twitter if you do not want to miss it.

Support for Symfony’s autowiring and autoconfiguration

Ever since I have started working with Behat and Symfony on more complex projects, the number of configuration changes needed to implement even simple steps was an issue and it was going worse and worse.

Whether it was Behat/Symfony2Extension or FriendsOfBehat/SymfonyExtension, injecting a dependency into a context required too much hassle and pulled me away from the domain being modelled.

The common use case is to have a context which implements a step like Given I am a logged in customer that is used in every suite which does web acceptance testing.

Let’s assume we need two Behat suites using the same context depending on kernel service and compare the required configuration for every Symfony extension for Behat out there.

Behat/Symfony2Extension

# behat.yml
first_suite:
    contexts:        - FeatureContext:            kernel: '@kernel'            
second_suite:
    contexts:        - FeatureContext:            kernel: '@kernel'

This snippet does not look that bad, mostly because of its size. It gets longer and longer with every new dependency or suite though. Adding a new dependency to a context requires changing the configuration of every suite.

FriendsOfBehat/SymfonyExtension v1

Finding all places where a context is used and modifying its dependencies is a boring work, which I wanted to avoid in the first version of FOB’s SymfonyExtension.

It was possible by registering contexts as services, which fundamentally changes the way you use them in Behat. This is why I decided to write my own extension to integrate Behat with Symfony.

# behat.yml
first_suite:
    contexts_services:        - FeatureContext            
second_suite:
    contexts_services:        - FeatureContext
# Symfony services file loaded by the extension
services:
    FeatureContext:
        arguments:
            - '@__symfony__.kernel'        tags: ['fob.context_service']

The duplication is removed, but it has introduced more boilerplate code and own conventions which are hard to get at first:

  • __symfony__ prefix when referencing service from Symfony application container prevented autocomplete in IDEs from working correctly

  • fob.context_service tag was required by the internal infrastructure to be able to use a service as a context, but it caused bugs quite often as it was so easy to forget it

  • contexts_services instead of default contexts has not introduced any measurable benefit, it was supposed to prevent conflicts with the default configuration if one does not use context services in every suite

The whole process of registering contexts manually was far from pleasant.

FriendsOfBehat/SymfonyExtension v2

The newest version reduces required boilerplate code by incorporating autowiring and autoconfiguration as first-class mechanisms and sticking to the already known conventions.

# behat.yml
first_suite:
    contexts:        - App\Tests\Behat\FeatureContext            
second_suite:
    contexts:        - App\Tests\Behat\FeatureContext

If your context constructor has kernel argument type-hinted, this is all you need to make it work.

No need to worry about duplicated configuration, weird conventions, tagging contexts or using prefixes when referring to services. And you can inject private services as well!

The extension provides a dependency injection configuration which automatically finds your contexts and autowires them:

# config/services_test.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\Tests\Behat\:
        resource: '../tests/Behat/*'

The following file is loaded only in the test environment. Your production and development environments are not affected with any performance loss. However, if you prefer not to rely on autowiring, you can define the context as a service on your own - remember to make it public though:

# config/services_test.yaml
services:
    App\Tests\Behat\FeatureContext:
        arguments:
            - '@kernel'
        public: true

Symfony Flex recipe

If you use Symfony Flex to manage your Symfony application, installation and configuration of the extension could not be easier.

All you need to do is to install the extension and agree for installing a contrib recipe for it:

composer install friends-of-behat/symfony-extension:^2.0 --dev
composer exec behat

In less than a minute you have a working setup of Behat with SymfonyExtension and you are ready to write your features.

If you do not use Symfony Flex, follow the installation documentation.

Zero-configuration for most Symfony 3 and 4 based apps

Whether you use Symfony 3 or Symfony 4, the new extension got you covered.

It will try to guess the sensible default configuration and in most cases, you will not need to write any more configuration than the following (assuming Flex have not done it for you):

# behat.yml
default:
    extensions:
        FriendsOfBehat\SymfonyExtension: ~

Learn more about the default settings.

Mink integration

This extension provides a driver for Mink based on BrowserKit called symfony.

If you switch from Behat\Symfony2Extension, the behaviour stays mostly the same. There is only one minor implementation detail that is worth to know.

symfony2 driver from the former extension uses the same kernel for both fetching application services and handling requests. This might lead to unexpected results if your code is not idempotent and relies on services state.

Given the following scenario and its implementation:

# Please do not write scenarios like this
# It is just for demonstration purposes
Given the service state is set to 42
Then I should get 42 from the API
And I should get 42 from the API
# Context implementation
/**
 * @Given the service state is set to :state
 */
public function serviceStateIs(string $state): void
{
    $this->service->setState($state);
}

/**
 * @Then I should get :state from the API
 */
public function iShouldGetFromAPI(string $state): void
{
    $this->minkSession->visit('/api/');
    assert($state === $this->minkSession->getPage()->getContent());
}
# Symfony controller
public function __invoke(): Response
{
    return new Response($this->service->getState());
}

This scenario would fail on the second Then step when using Behat\Symfony2Extension.

It happens because the kernel is rebooted after each request, so that the first Then step one uses the same container that is used in Behat Given step before, but the second Then step uses a rebooted container which does not share the same state. This might make your tests fragile, eg. when you change assertions order.

When run with FriendsOfBehat\SymfonyExtension, the scenario would fail on the first Then step because the service state is not shared between Behat and HTTP application.

If you want to learn more, check out the documentation about differences between Symfony extensions or the documenation about Mink integration.

What’s next?

With all those new, shiny features mentioned above, there are still a few ideas in my mind that would be great to see in the future:

I made a GitHub project which is used as a roadmap for this project.

Kamil Kokot

I'm Kamil Kokot. My goal is to make software testing as effortless as possible so that you can develop with confidence. I work on Sylius and FriendsOfBehat.