Behat Mink: 07 - Context Class

 

In order to be used by Behat, your context class should follow these rules:

  1. The context class should implement the Behat\Behat\Context\Context interface.
  2. The context class should be called FeatureContext. It’s a simple convention inside the Behat infrastructure. FeatureContext is the name of the context class for the default suite. This can easily be changed through suite configuration inside behat.yml.
  3. The context class should be discoverable and loadable by Behat. That means you should somehow tell Behat about your class file. Behat comes with a PSR-0 autoloader out of the box and the default autoloading directory is features/bootstrap. That’s why the default FeatureContext is loaded so easily by Behat. You can place your own classes under features/bootstrap by following the PSR-0 convention or you can even define your own custom autoloading folder via behat.yml.

Behat\Behat\Context\SnippetAcceptingContext and Behat\Behat\Context\CustomSnippetAcceptingContext are special versions of the Behat\Behat\Context\Context interface that tell Behat this context expects snippets to be generated for it.

The Behat command line tool has an --init option that will initialize a new Behat project in your directory. Learn more about Initialize a New Behat Project.

Contexts Lifetime

Your context class is initialized before each scenario is run, and every scenario has its very own context instance. This means 2 things:

  1. Every scenario is isolated from each other scenario’s context. You can do almost anything inside your scenario context instance without the fear of affecting other scenarios - every scenario gets its own context instance.
  2. Every step in a single scenario is executed inside a common context instance. This means you can set private instance variables inside your @Given steps and you’ll be able to read their new values inside your @When and @Then steps.

Multiple Contexts

At some point, it could become very hard to maintain all your step definitions and Hooks inside a single class. You could use class inheritance and split definitions into multiple classes, but doing so could cause your code to become more difficult to follow and use.

In light of these issues, Behat provides a more flexible way of helping make your code more structured by allowing you to use multiple contexts in a single test suite.

In order to customise the list of contexts your test suite requires, you need to fine-tune the suite configuration inside behat.yml:

# behat.yml

default:
    suites:
        default:
            contexts:
                - FeatureContext
                - SecondContext
                - ThirdContext

The first default in this configuration is a name of the profile. We will discuss profiles a little bit later. Under the specific profile, we have a special suites section, which configures suites inside this profile. We will talk about test suites in more detail in the next chapter, for now just keep in mind that a suite is a way to tell Behat where to find your features and how to test them. The interesting part for us now is the contexts section - this is an array of context class names. Behat will use the classes specified there as your feature contexts. This means that every time Behat sees a scenario in your test suite, it will:

  1. Get list of all context classes from this contexts option.
  2. Will try to initialize all these context classes into objects.
  3. Will search for step definitions and Hooks in all of them.

Do not forget that each of these context classes should follow all context class requirements. Specifically - they all should implement Behat\Behat\Context\Context interface and be autoloadable by Behat.

Basically, all contexts under the contexts section of your behat.yml are the same for Behat. It will find and use the methods in them the same way it does in the default FeatureContext. And if you’re happy with a single context class, but you don’t like the name FeatureContext, here’s how you change it:

# behat.yml

default:
    suites:
        default:
            contexts:
                - MyAwesomeContext

This configuration will tell Behat to look for MyAwesomeContext instead of the default FeatureContext.

Unlike profiles, Behat will not inherit any configuration of your default suite. The name default is only used for demonstration purpose in this guide. If you have multiple suites that all should use the same context, you will have to define that specific context for every specific suite:

# behat.yml

default:
    suites:
        default:
            contexts:
                - MyAwesomeContext
                - MyWickedContext
        suite_a:
            contexts:
                - MyAwesomeContext
                - MyWickedContext
        suite_b:
            contexts:
                - MyAwesomeContext

This configuration will tell Behat to look for MyAwesomeContext and MyWickedContext when testing suite_a and MyAwesomeContext when testing suite_b. In this example, suite_b will not be able to call steps that are defined in the MyWickedContext. As you can see, even if you are using the name default as the name of the suite, Behat will not inherit any configuration from this suite.

Context Parameters

Context classes can be very flexible depending on how far you want to go in making them dynamic. Most of us will want to make our contexts environment-independent; where should we put temporary files, which URLs will be used to access the application? These are context configuration options highly dependent on the environment you will test your features in.

We already said that context classes are just plain old PHP classes. How would you incorporate environment-dependent parameters into your PHP classes? Use constructor arguments:

// features/bootstrap/MyAwesomeContext.php

use Behat\Behat\Context\Context;

class MyAwesomeContext implements Context
{
    public function __construct($baseUrl, $tempPath)
    {
        $this->baseUrl = $baseUrl;
        $this->tempPath = $tempPath;
    }
}

As a matter of fact, Behat gives you ability to do just that. You can specify arguments required to instantiate your context classes through same contexts setting inside your behat.yml:

# behat.yml

default:
    suites:
        default:
            contexts:
                - MyAwesomeContext:
                    - http://localhost:8080
                    - /var/tmp

Note an indentation for parameters. It is significant:

contexts:
    - MyAwesomeContext:
        - http://localhost:8080
        - /var/tmp

Aligned four spaces from the context class itself.

Arguments would be passed to the MyAwesomeContext constructor in the order they were specified here. If you are not happy with the idea of keeping an order of arguments in your head, you can use argument names instead:

# behat.yml

default:
    suites:
        default:
            contexts:
                - MyAwesomeContext:
                    baseUrl: http://localhost:8080
                    tempPath: /var/tmp

As a matter of fact, if you do, the order in which you specify these arguments becomes irrelevant:

# behat.yml

default:
    suites:
        default:
            contexts:
                - MyAwesomeContext:
                    tempPath: /var/tmp
                    baseUrl: http://localhost:8080

Taking this a step further, if your context constructor arguments are optional:

public function __construct($baseUrl = 'http://localhost', $tempPath = '/var/tmp')
{
    $this->baseUrl = $baseUrl;
    $this->tempPath = $tempPath;
}

You then can specify only the parameter that you actually need to change:

# behat.yml

default:
    suites:
        default:
            contexts:
                - MyAwesomeContext:
                    tempPath: /var/tmp

In this case, the default values would be used for other parameters.

Context Traits

PHP 5.4 have brought an interesting feature to the language - traits. Traits are a mechanism for code reuse in single inheritance languages like PHP. Traits are implemented as a compile-time copy-paste in PHP. That means if you put some step definitions or hooks inside a trait:

// features/bootstrap/ProductsDictionary.php

trait ProductsDictionary
{
    /**
     * @Given there is a(n) :product, which costs £:price
     */
    public function thereIsAWhichCostsPs($product, $price)
    {
        throw new PendingException();
    }
}

And then use it in your context:

// features/bootstrap/MyAwesomeContext.php

use Behat\Behat\Context\Context;

class MyAwesomeContext implements Context
{
    use ProductsDictionary;
}

It will just work as you expect it to.

Context traits come in handy if you’d like to have separate contexts, but still need to use the very same step definition in both of them. Instead of having the same code in both context classes – and having to maintain it in both – you should create a single Trait that you would then use in both context classes.

Given that step definitions cannot be duplicated within a Suite, this will only work for contexts used in separate suites.

In other words, if your Suite uses at least two different Contexts, and those context classes use the same Trait, this will result in a duplicate step definition and Behat will complain by throwing a Redundant exception.

 


 

DrupalExtenstion Contexts

Adding Contexts to the classpath provides additional hooks and functionality to the test script

  • RawDrupalContext
    • A context that provides no step definitions, but all of the necessary functionality for interacting with Drupal, and with the browser via Mink sessions.
  • DrupalContext
    • Provides step-definitions for creating users, terms, and nodes.
  • MinkContext
    • Builds on top of the Mink Extension and adds steps specific to regions and forms.
  • MarkupContext
    • Contains step definitions that deal with low-level markup (such as tags, classes, and attributes).
  • MessageContext
    • Step-definitions that are specific to Drupal messages that get displayed (notice, warning, and error).
  • DrushContext
    • Allows steps to directly call drush commands.

 

Tags