Configuration
This page explains where each Behat Steps setting goes and how settings in different places combine. Read it when you set up a suite or change what a trait does. To look up a setting you already know, see Configuration reference, which lists every extension option, tag and environment variable with its type and default.
Behat Steps shares 3 settings with Mink: base_url, files_path and each browserkit_http session's http_client_parameters. It reads the rest of its configuration through 4 channels, and each channel answers a different question:
| Channel | Answers | Scope |
|---|---|---|
| Suites and context arguments | Which scenarios run against which surface, with which steps | Per suite |
The behat_steps extension key | How the package talks to the site | Per profile |
| Tags | What a single scenario opts into | Per scenario or feature |
| Environment variables | What differs between one machine and another | Per run |
1. Suites and context arguments
2 settings live per suite: which feature files run with which contexts, and the constructor arguments each context takes.
Suite per surface
A suite pairs a set of feature files with a set of contexts. The quick start uses 1 suite with your FeatureContext in it, and many projects never need another. When you test more than 1 surface, such as pages in a browser and an API, you can give each surface a suite of its own. In this behat.php, every suite registers the same FeatureContext:
<?php
declare(strict_types=1);
use Behat\Config\Config;
use Behat\Config\Extension;
use Behat\Config\Profile;
use Behat\Config\Suite;
use Behat\MinkExtension\Context\MinkContext;
use DrevOps\BehatSteps\Extension\ServiceContainer\BehatStepsExtension;
// The browser surface: what an editor or a visitor can do in a page.
$ui = (new Suite('ui'))
->withPaths('%paths.base%/tests/behat/features/ui')
->addContext(FeatureContext::class)
->addContext(MinkContext::class);
// The API surface: responses, headers and payloads, with no browser.
$api = (new Suite('api'))
->withPaths('%paths.base%/tests/behat/features/api')
->addContext(FeatureContext::class);
// The specification surface: the few domain-language scenarios a
// stakeholder reads.
$spec = (new Suite('spec'))
->withPaths('%paths.base%/tests/behat/features/spec')
->addContext(FeatureContext::class);
$profile = (new Profile('default', [
'autoload' => ['%paths.base%/tests/behat/bootstrap'],
]))
->withSuite($ui)
->withSuite($api)
->withSuite($spec)
->withExtension(new Extension(BehatStepsExtension::class, [
'drupal' => ['drupal_root' => 'web'],
]));
return (new Config())->withProfile($profile);
Run 1 surface with vendor/bin/behat --suite=api, or all of them with a bare vendor/bin/behat.
The layout gives you 3 things:
- The spec suite and the regression suites do different jobs. The spec suite holds domain-language scenarios written over your own step definitions, and the regression suites hold broad scenarios written in the shipped steps. Scenario styles explains why both exist.
- You can leave a slow surface out of a run and keep the others. Put accessibility, visual and
@javascriptscenarios behind their own suite or a tag filter, so the fast suites stay fast. - A surface can offer only the steps that work on it. Register a narrower context there in place of
FeatureContext, composed fromWebRawContext. An API suite built onRestTraitandJsonTraitloads no trait that needs a rendered page. Behat then reports a step that can't work on the surface as undefined, so the step never runs into a confusing failure.
Context constructor arguments
The extension key in section 2 holds 1 set of settings per profile. Pass a value that has to differ between suites as a context constructor argument.
WebRawContext declares 1 argument of its own, config. Every context that extends WebRawContext, shipped or your own, inherits it without redeclaring a constructor. config carries the trait options that differ between 2 suites of the same profile:
$ui = (new Suite('ui'))
->withPaths('%paths.base%/tests/behat/features/ui')
->addContext(FeatureContext::class, [
'config' => ['wait' => ['ajax_timeout' => 10]],
]);
Behat passes a context only the arguments on its own registration, so config goes on the registration of FeatureContext, the class your suite registers. An argument registered for DrupalContext or WebContext never reaches a FeatureContext that extends it.
Each key of config is a group of options, named after the trait that declares it. A context accepts only the groups of its own traits, counting the traits of the classes it extends. Any other group fails the context at construction, with an error that names the groups it accepts.
WebContext takes the groups of the web traits, such as wait and javascript. DrupalContext adds the groups of the Drupal traits, such as watchdog, big_pipe, cache, queue and email, so a FeatureContext that extends it takes them all.
A context can take arguments of its own too. It declares them in its constructor and forwards config to the parent. A default keeps an argument optional, so Behat still builds the context for a suite that doesn't pass it, like the api suite in the first example:
class FeatureContext extends DrupalContext {
public function __construct(
protected ?string $fixtures_path = NULL,
array $config = [],
) {
parent::__construct($config);
}
}
Behat matches the arguments to the constructor by name, so the array keys are the parameter names. This ui suite passes both:
$ui = (new Suite('ui'))
->withPaths('%paths.base%/tests/behat/features/ui')
->addContext(FeatureContext::class, [
'fixtures_path' => '%paths.base%/tests/behat/fixtures',
'config' => ['javascript' => ['fail_on_errors' => FALSE]],
]);
Behat 4 reads PHP configuration only: behat.php, or behat.dist.php when there's no behat.php. Behat 3 also reads the same settings from behat.yml. Prefer behat.php, because both majors accept it.
On Behat 3, the ui suite with its fixtures_path argument and the api suite look like this in behat.yml. A config argument goes beside fixtures_path the same way:
default:
suites:
ui:
paths: ['%paths.base%/tests/behat/features/ui']
contexts:
- FeatureContext:
fixtures_path: '%paths.base%/tests/behat/fixtures'
api:
paths: ['%paths.base%/tests/behat/features/api']
contexts: [FeatureContext]
extensions:
DrevOps\BehatSteps\Extension\ServiceContainer\BehatStepsExtension:
drupal:
drupal_root: web
behat.dist.php in the Behat Steps repository sets every extension option, plus the trait options most projects set, as a reference.
2. The behat_steps extension key
Settings under this key configure how the package reaches the site. They apply to the whole profile and set the following:
- Which backends a scenario can resolve, and in what order
- How each backend connects
- What the login form looks like
- Which CSS selector each named region resolves to
- What each trait option defaults to
behat_steps is the name the extension gives its settings, and you don't write it anywhere. In behat.php, the settings are the array you pass to new Extension() with BehatStepsExtension::class, as the following examples show. In behat.yml, they're the map under the extension's class name.
HTTP connection settings are the exception. Certificate checks, a proxy or DNS pinning live on Mink's browserkit_http session as http_client_parameters. Requests that steps send from PHP, such as a file download, share them with the page. HTTP clients covers how.
For example, this profile allows 2 backends, sets where Drupal lives and names 1 region:
$profile->withExtension(new Extension(BehatStepsExtension::class, [
'backends' => ['drupal', 'blackbox'],
'drupal' => ['drupal_root' => 'web'],
'regions' => ['content' => '#content'],
]));
Extension options lists every option under the key, with its type and default.
Backend resolution
5 authorities decide which backend runs a step, and each decides exactly 1 thing:
| Authority | Decides | Where |
|---|---|---|
| Extension | Which backends exist at all, and in what order | 'backends' => [...] |
| Profile | Which backends exist per environment | behat -p remote |
| Scenario tag | Preference among the backends that exist | @backend:NAME |
| Step | Which capability it needs | backendFor(X::class) |
| Capability interface | Which backends are even eligible | instanceof during the walk |
The backends list is both the allowlist and the precedence order. Each entry names a backend the extension registers. A keyed entry gives the backend a name of its own, so the same feature file can run against a different backend in another profile. In this profile, api names the backend registered as acme-jsonapi:
$profile->withExtension(new Extension(BehatStepsExtension::class, [
'blackbox' => NULL,
'drupal' => ['drupal_root' => 'web'],
'backends' => [
'drupal',
'api' => 'acme-jsonapi',
'blackbox',
],
]));
'blackbox' => NULL changes nothing, since the blackbox backend is always registered. Aliasing a backend as api lets a profile swap the implementation behind the name without touching any Gherkin:
// Default profile: "api" is the in-process backend.
'backends' => ['api' => 'drupal', 'blackbox'],
// "behat -p staging": "api" is the real HTTP API, and Drupal is unreachable.
'backends' => ['api' => 'acme-jsonapi', 'blackbox'],
A name is unique, and it holds only letters, digits, _ and - so that @backend:NAME is a valid tag. A name the extension doesn't register fails the container build. A configuration that declares no list gets every registered backend, in registration order.
acme-jsonapi here stands for a backend of your own. A backend of your own shows how to write and register one.
To restrict a run to fewer backends, give it a profile of its own. The package reads nothing from a suite's own settings, such as its paths.
At step time the resolution is:
- Start from the configured order.
- Move every name a
@backend:tag promotes to the front: the names on the scenario line first, then those on the feature line. Each line's names keep the configured order, so the order the tags are written in never changes the result. - Walk that order and return the first backend implementing the capability the step asked for, bootstrapping only that one.
For example, with the backends list drupal, api and blackbox that the acme-jsonapi example sets, this scenario promotes api:
@backend:api
Scenario: Content created over the public API is immediately visible
Given the following "article" content exists:
| title |
| Lab report |
When I visit "/articles"
Then I should see "Lab report"
The order becomes api, drupal, blackbox, and the content step resolves to api, the acme-jsonapi backend. A cache step in the same scenario still resolves to drupal, because acme-jsonapi implements no cache capability. Promotion only affects the capabilities the promoted backend provides.
Repeated tags promote their names in the configured order: @backend:blackbox @backend:api and @backend:api @backend:blackbox both give api, blackbox, drupal. A Scenario Outline row counts the outline's tags and its Examples: table's tags as 1 scenario line.
@backend:NAME takes a name as the backends list writes it. A keyed entry's name is its key, such as api. A bare entry's name is the entry itself. The tag reorders the configured list and never adds to it. A name outside the list stops the whole run at that scenario's start, so a typo can't silently run the wrong backend. No tag can hand a Drupal backend to a smoke profile that lists only blackbox. When no backend in the order provides the capability a step asked for, the step fails with an UnsupportedBackendActionException naming the capability and the resolved order.
The backends also take classes of your own through the drupal.field_handlers, drupal.creation_aliases, drush.creation_aliases and drupal.core options, which Extending the backend layer covers.
Trait options
Every configurable trait declares its own options, and the steps section holds their profile-wide defaults. A group is named after the trait that declares it, in snake case: JavascriptTrait reads javascript, BigPipeTrait reads big_pipe and FileDownloadTrait reads file_download. A trait's page under Web steps or Drupal steps lists its options beside its steps. This profile sets defaults for 5 groups:
$profile->withExtension(new Extension(BehatStepsExtension::class, [
'drupal' => ['drupal_root' => 'web'],
'steps' => [
'javascript' => ['enabled' => TRUE, 'fail_on_errors' => TRUE],
'watchdog' => ['enabled' => TRUE, 'fail_on_errors' => TRUE],
'big_pipe' => ['wait_timeout' => 5000],
'wait' => ['ajax_timeout' => 10],
'modal' => ['selectors' => ['.ui-dialog', '[role="dialog"]']],
],
]));
wait.ajax_timeout is in seconds and big_pipe.wait_timeout in milliseconds, as each trait's page lists.
An option resolves through 5 layers, each overriding the one before it:
- The default the trait declares.
behat_steps.steps, for the whole profile.- The context's
configargument, for 1 context. - A tag on the feature.
- A tag on the scenario.
These tags set an option for their scenario or feature:
@behat-steps-skip:TRAITsets theenabledoption of the trait it names toFALSE. It names the trait by its class name, such asWatchdogTrait, not by its group.@js-errorssetsjavascript.fail_on_errorstoFALSE.@errorsetswatchdog.fail_on_errorstoFALSE.- An
@accessibility:VALUEthreshold setsaccessibility.failure_threshold, and@accessibility:strictsetsaccessibility.fail_on_incompletetoTRUE.
Apart from the @accessibility: tags, these tags only switch an option off, so no tag switches back on what the profile or a context's config switched off.
The 2 levels, steps and a context's config, differ in how strictly they're read:
stepsis permissive. A context ignores a group or an option there that it doesn't declare, so 1 profile can carry the defaults of every suite. A value of the wrong type for an option the context does declare still fails it.- A context's
configis strict. It's checked against the traits that context composes. An unknown group, an unknown option or a value of the wrong type is an error that names what the context accepts.
2 options recur, and they're deliberately separate switches:
| Option | Effect of FALSE |
|---|---|
enabled | The trait's hooks switch off, and its steps still work. Equivalent to @behat-steps-skip:TRAIT on every scenario. |
fail_on_errors | The trait still collects the errors it checks for, such as JavaScript errors or Watchdog entries, and doesn't fail the scenario on them. |
fail_on_errors only covers what a trait collected. A trait whose prerequisites don't hold fails the scenario at its start, whatever fail_on_errors is set to. WatchdogTrait does that on a site without the core dblog module, so switch it off with enabled there. Switch a trait off has the details.
Where a trait offers a <trait>Get<Noun>() method for an option, overriding it in your context sits outside these layers and replaces the resolution entirely. Use that override for anything the configuration can't express.
Extending the step vocabulary covers declaring options for a trait of your own, and Extending the extension layer covers replacing the resolution itself.
3. Tags
A tag configures 1 scenario or 1 feature. A tag's value follows a colon, and a scenario can carry several tags:
@module:redirect @behat-steps-skip:WatchdogTrait
Scenario: Editor publishes a page
A tag on the Feature: line applies to every scenario in that feature. Where a scenario and its feature carry the same kind of tag, they combine as follows:
- A flag tag, such as
@download, takes no value and takes effect from either line. @behat-steps-skip:VALUE,@email:VALUE,@watchdog:VALUE,@disable-config-override:VALUEand@behat-steps-entity-cleanup-skip:VALUEadd up across both lines.@module:VALUEfor the same module,@breakpoint:VALUEand an@accessibility:VALUEthreshold take the scenario's value over the feature's.@accessibility:strictapplies from either line. A!before a module's name uninstalls it. A scenario's@module:!redirectwins over its feature's@module:redirect, so the scenario runs with the module uninstalled.@backend:VALUEpromotes from both lines, the scenario's names ahead of the feature's, as backend resolution describes.
Tags lists every tag and what it does, with the rules for writing a tag's value.
@javascript is Mink's own tag: Mink runs a scenario that carries it on Mink's JavaScript session. Backend resolution and the @backend:NAME tag decide which backend a step runs against.
4. Environment variables
An environment variable changes 1 run, or 1 machine, without changing any committed configuration. Behat Steps reads 3 of its own. BEHAT_STEPS_DISABLE_CLEANUP, for example, keeps what a scenario created so you can inspect it. Environment variables lists all 3.
BEHAT_PARAMS is Behat's own variable. It holds a JSON object of settings that Behat merges with your configuration file. It can only add a setting the file leaves out, because Behat loads BEHAT_PARAMS first and the file after it.
For example, when the chrome session in behat.php has no api_url, this command supplies one:
BEHAT_PARAMS='{"extensions": {"Behat\\MinkExtension": {"sessions":
{"chrome": {"chrome": {"api_url": "http://chrome:9222"}}}}}}' \
vendor/bin/behat
The extension key can be Mink's short name, Behat\MinkExtension. It can also be the class name behat.php registers, Behat\MinkExtension\ServiceContainer\MinkExtension. Both reach the same settings. In JSON, write each backslash in them twice.
To change a value the file does set, for 1 run, add a profile that overrides it and pick that profile with -p. A profile other than default starts from default, so it names only what differs. Installation shows one for a headed Chrome.