Creating a Behat extension to provide custom configuration for Contexts

Extensions are particularly useful when configuration becomes a necessity.

In this cookbook, we will create a simple extension named HelloWorld that will display some text and cover:

  1. Setting up the context

  2. Creating the extension

  3. Initializing the context

  4. Using the extension

Setting Up the Context

First, we create a Context class that will throw a PendingException with a configurable text. The configuration will also control whether the behaviour is enabled or not.

src/
    Context/
        HelloWorldContext.php   # This is where we'll implement our step
<?php

namespace HelloWorld\Context;

use Behat\Behat\Context\Context as BehatContext;
use Behat\Behat\Tester\Exception\PendingException;

class HelloWorldContext implements BehatContext
{
    private bool $enable = false;
    private string $text;

    /*
     * Will be used by the extension to initialise the context with configuration values.
     */
    public function initializeConfig(bool $enable, string $text)
    {
        $this->enable = $enable;
        $this->text = $text;
    }

    /** @Given I say Hello World */
    public function helloWorld()
    {
        if ($this->enable) {
          throw new PendingException($this->text);
        }
    }
}

Creating the Extension

Next, we need to create the entry point for our Hello World, the extension itself.

src/
    Context/
        HelloWorldContext.php
    ServiceContainer/
        HelloWorldExtension.php   # This is where we'll define our extension

The getConfigKey method below is used to identify our extension in the configuration. The configure method is used to define the configuration tree.

<?php

namespace HelloWorld\ServiceContainer;

use Behat\Behat\Context\ServiceContainer\ContextExtension;
use Behat\Testwork\ServiceContainer\Extension;
use Behat\Testwork\ServiceContainer\ExtensionManager;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use HelloWorld\Context\Initializer\HelloWorldInitializer;

class HelloWorldExtension implements Extension
{
    public function getConfigKey()
    {
        return 'helloworld_extension';
    }

    /**
     * Called after extensions activation, but before `configure()`.
     * Used to hook into other extensions' configuration.
     */
    public function initialize(ExtensionManager $extensionManager)
    {
        // empty for our case
    }

    public function configure(ArrayNodeDefinition $builder)
    {
        $builder
            ->addDefaultsIfNotSet()
                ->children()
                    ->booleanNode('enable')->defaultFalse()->end()
                    ->scalarNode('text')->defaultValue('Hello World!')->end()
                ->end()
            ->end();
    }

    public function load(ContainerBuilder $container, array $config)
    {
        // ... we'll load our configuration here
    }

    // needed as Extension interface implements CompilerPassInterface
    public function process(ContainerBuilder $container)
    {
    }
}

Note

The initialize and process methods are empty in our case but are useful when you need to interact with other extensions or process the container after it has been compiled.

Initializing the Context

To pass configuration values to our HelloWorldContext, we need to create an initializer.

src/
    Context/
        Initializer/
            HelloWorldInitializer.php   # This will handle context initialization
          HelloWorldContext.php
    ServiceContainer/
      HelloWorldExtension.php

The code for HelloWorldInitializer.php:

<?php

namespace HelloWorld\Context\Initializer;

use HelloWorld\Context\HelloWorldContext;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\Initializer\ContextInitializer;

class HelloWorldInitializer implements ContextInitializer
{
    private string $text;
    private bool $enable;

    public function __construct(string $text, bool $enable)
    {
        $this->text = $text;
        $this->enable = $enable;
    }

    public function initializeContext(Context $context)
    {
        /*
         * At the start of every scenario, behat will create a new instance of every `Context`
         * registered in your project. It will then call this method with each new `Context` in
         * turn. If you want to initialise multiple contexts, you can of course give them an
         * interface and check for that here.
         */
        if (!$context instanceof HelloWorldContext) {
            return;
        }

        $context->initializeConfig($this->enable, $this->text);
    }
}

We need to register the initializer definition within the Behat container through the HelloWorldExtension, ensuring it gets loaded:

<?php

// ...

use Symfony\Component\DependencyInjection\Definition;
use Behat\Behat\Context\ServiceContainer\ContextExtension;

class HelloWorldExtension implements Extension
{
    // ...

    public function load(ContainerBuilder $container, array $config)
    {
        $definition = new Definition(HelloWorldInitializer::class, [
            $config['text'],
            $config['enable'],
        ]);
        $definition->addTag(ContextExtension::INITIALIZER_TAG);
        $container->setDefinition('helloworld_extension.context_initializer', $definition);
    }

    // ...
}

Using the extension

Now that the extension is ready and will inject values into context, we just need to configure it into a project.

In the extensions key of a profile (default in our case), we’ll add the HelloWorldExtension key and configure our text and enable value.

Finally, we need to load the HelloWorld\Context\HelloWorldContext into our suite.

Here’s the behat.yaml:

default:
  suites:
    default:
      contexts:
        - FeatureContext
        - HelloWorld\Context\HelloWorldContext
  extensions:
    HelloWorld\ServiceContainer\HelloWorldExtension:
      text: 'Hi there!'
      enable: true

And now a scenario like this one:

Feature: Test

  Scenario: Test
    Given I say Hello World

Will display our text Hi there! as a pending exception.

Conclusion

Congratulations! You have just created a simple Behat extension. This extension demonstrates three of the common steps to building a Behat extension: defining an extension, creating an initializer, and configuring contexts.

Feel free to experiment with this extension and expand its functionality.

Happy testing!

Previous chapter
Accessing Contexts from each other
Next chapter
Releases & version support