Skip to main content

Contexts

A context is the PHP class Behat finds step definitions in, as Key concepts explains. Behat Steps ships 3 of them. Your project extends exactly 1 of them in a class of its own, FeatureContext. Your own steps go in FeatureContext or in traits it uses.

This page shows what each context includes, which one to extend and where your own steps go. Usage covers configuring the traits inside your context.

What each context includes​

Each context builds on the one before it:

  • WebRawContext extends Mink's RawMinkContext and has no steps. It holds the plumbing every step relies on: the backends, the browser's capabilities, the 4 HTTP clients, trait options, prerequisite checks and hook dispatch. It comes with 3 helper traits too: LastStepTrait, RequestHeadersTrait and StringTrait.
  • WebContext extends WebRawContext and adds every trait under Steps\Web: the 30 traits of web steps, which work on any website.
  • DrupalContext extends WebContext and adds every trait under Steps\Drupal: the 29 traits of Drupal steps, and the user registry their login steps need.

A step trait also brings the helper traits it calls. The traits that create content, for example, bring EntityLifecycleTrait, which deletes what a scenario created. Steps layer draws how it all fits together.

Which one to extend​

Pick the context that matches your site:

  • On a Drupal site, extend DrupalContext. It inherits every web step from WebContext, including I visit from PathTrait. It also inherits the value transforms of MappingTrait, RandomTrait and DateTrait: {{ }}, [?...] and [relative:...]. EntityTrait adds a fourth, [id:...], which replaces an entity's label with its ID. On a site without the core dblog module, the Watchdog check of DrupalContext fails every scenario until you enable the module or switch the check off.
  • On any other site, extend WebContext.
  • Extend WebRawContext only when you want to pick every trait yourself, as Compose your own context shows.

FeatureContext lives in tests/behat/bootstrap/FeatureContext.php, and on a Drupal site it can start empty:

<?php

declare(strict_types=1);

use DrevOps\BehatSteps\DrupalContext;

class FeatureContext extends DrupalContext {

}

Register FeatureContext and Mink's MinkContext in your suite, never WebContext or DrupalContext beside them. The quick start shows the whole behat.php.

Why 1 context holds every step​

A Behat suite can register any number of contexts, and many step libraries ship 1 context per topic for you to combine. Behat Steps puts every step on 1 context, for 3 reasons:

  • There's nothing to pick. Every shipped step is on the context you extend, so a scenario can use any of them without a change to your PHP.
  • Your own steps have 1 home. With several contexts, you pick a context for every new step. A step in 1 context also can't call a helper that only another context has. With 1 FeatureContext, every helper is on $this.
  • Nothing gets defined twice. Behat registers a trait's steps for each registered context that has the trait. It stops with an error when 2 definitions share the same text. Registering 2 contexts built on WebContext, such as DrupalContext beside your FeatureContext, would define every web step twice. WebContext::assertOneContext() stops that run before the first scenario and names the context to drop.

Having every step costs little. A trait for a contrib module looks for its module only when one of its steps runs. Some checks can fail a scenario for a reason unrelated to what the scenario tests, such as the Watchdog check. Each of those checks carries an enabled option you can switch off in behat.php. An inherited step can't be removed. When a step of yours needs the exact text of a shipped one, reword yours or compose your own context without the trait that ships it.

Your own steps in traits of your own​

A step of your own can go straight into FeatureContext, as the quick start shows. When you have more than a few, group them into traits, 1 per area of your site, and have FeatureContext use them. The library groups its own steps the same way. With 1 trait, the layout looks like this:

tests/behat/
├── bootstrap/
│ ├── FeatureContext.php
│ └── PublishingTrait.php
└── features/
└── publishing.feature

FeatureContext uses the trait:

<?php

declare(strict_types=1);

use DrevOps\BehatSteps\DrupalContext;

class FeatureContext extends DrupalContext {

use PublishingTrait;

}

The trait holds the steps, each built on a helper:

<?php

declare(strict_types=1);

use Behat\Step\Given;
use Behat\Step\When;

/**
* @phpstan-require-extends \DrevOps\BehatSteps\DrupalContext
*/
trait PublishingTrait {

#[Given('a page titled :title exists')]
public function publishingCreatePage(string $title): void {
$this->contentCreate('page', ['title' => $title]);
}

#[Given('I am a site administrator')]
public function publishingLoginAsAdministrator(): void {
$this->userCreateAndLogin('administrator');
}

#[When('I open the page titled :title')]
public function publishingVisitPage(string $title): void {
$this->contentVisitActionPageWithTitle('page', $title);
}

}

Here's what each part of the trait does:

  • Each step calls 1 of the library's helpers on $this. HELPERS.md lists them all. Call only the public ones: a protected method can change in any release.
  • The page and the administrator account these helpers create are deleted when the scenario ends, as they are for the shipped steps.
  • The @phpstan-require-extends line names the class the helpers come from, for your editor and PHPStan.
  • Every method name starts with publishing, from the trait's name, as the library's own methods do. So no method can collide with a shipped method or with another trait of yours.

Behat loads both files from tests/behat/bootstrap by their names, through the profile's autoload setting in behat.php:

$profile = (new Profile('default', [
'autoload' => ['%paths.base%/tests/behat/bootstrap'],
]))

A scenario in tests/behat/features/publishing.feature then reads in your site's own words:

Feature: Publishing

Scenario: Administrator opens a new page
Given a page titled "About us" exists
And I am a site administrator
When I open the page titled "About us"
Then I should see the heading "About us"

An AI coding agent writes steps like these with the create-behat-step skill, which ships with the package. Describe the step, and the agent writes it in a trait like this one and adds the trait to FeatureContext. Writing tests with AI assistants shows how to run the skill.

Scenario styles explains when steps in your own words pay off. When your trait needs options or hooks, Anatomy of a trait walks through a trait that has both.

Compose your own context​

A context built on WebRawContext takes only the traits you list:

<?php

declare(strict_types=1);

use DrevOps\BehatSteps\Steps\Web\JavascriptTrait;
use DrevOps\BehatSteps\Steps\Web\WaitTrait;
use DrevOps\BehatSteps\WebRawContext;

class UiContext extends WebRawContext {

use JavascriptTrait;
use WaitTrait;

}

Compose a context like this when a shipped step's text collides with a step you need to keep. Compose one also when a suite should offer only a few steps, such as an API suite built on RestTrait and JsonTrait. Each step trait brings the helper traits it needs, so composing ContentTrait also brings the entity cleanup that removes what its steps created.

1 profile carries the extension, and each suite registers its own context:

$ui = (new Suite('ui'))
->withPaths('%paths.base%/tests/behat/features/ui')
->addContext(UiContext::class);

$profile = (new Profile('default'))
->withSuite($ui)
->withExtension(new Extension(BehatStepsExtension::class, [
'backends' => ['drupal', 'blackbox'],
'drupal' => ['drupal_root' => 'web'],
]));

The rules that govern composition cover what Behat does when a context composes a trait twice or overrides an inherited step or hook.