Usage
This guide covers the traits inside your context day to day: how to configure them, how to switch a trait's checks off and what happens to the content your scenarios create. Contexts covers which context to extend and where your own steps go. If traits or backends are new to you, read Key concepts first.
Your context is built out of traits, and the extension connects it to your site. The extension and the context answer different questions, and each trait reads from both:
| Layer | Answers | Scope |
|---|---|---|
| The extension | How the package reaches the site, and what every trait defaults to | Per profile |
| The context | Which steps a suite gets, and which of those defaults it overrides | Per context |
| The trait | What a step does, and which options change it | Per trait |
Configuration reference lists every option and tag. This page explains how they fit together.
Configure a trait
Each configurable trait declares its own options, grouped under the trait's name in snake case. Their profile-wide defaults live under steps:
->withExtension(new Extension(BehatStepsExtension::class, [
'drupal' => ['drupal_root' => 'web'],
'steps' => [
'wait' => ['ajax_timeout' => 10],
'watchdog' => ['fail_on_errors' => TRUE],
],
]));
The message group is 1 you'll almost always set. Its steps, such as I should see the success message :message, need the CSS selector your site prints that message in, and its selectors option has no default:
'steps' => [
'message' => ['selectors' => [
'success' => '.messages.messages--status',
]],
],
selectors takes 1 selector per severity: default, error, success and warning. The steps that name no severity use default.
When a value has to differ between 2 suites of the same profile, put it in the context's config argument instead. A context accepts the groups of the traits it holds. FeatureContext inherits JavascriptTrait through DrupalContext, so it accepts the javascript group:
$ui = (new Suite('ui'))
->withPaths('%paths.base%/tests/behat/features/ui')
->addContext(FeatureContext::class, [
'config' => ['javascript' => ['fail_on_errors' => FALSE]],
]);
A single scenario opts out with the tag the option declares:
@javascript @js-errors
Scenario: The legacy report still renders
Given I visit "/reports/legacy"
When steps, a context's config and a tag set the same option, the most specific wins. From least to most specific, they're steps, the context's config, a feature tag and a scenario tag. Trait options states the full order. A trait's page under Web steps or Drupal steps lists its options beside its steps.
Switch a trait off
2 options recur across the traits that carry hooks, and they mean different things:
enabledturns the trait's hooks off entirely: nothing is collected, and no backend is bootstrapped on its account.fail_on_errorsleaves the collection running and stops what it collects from failing the scenario.
If you never want a trait's check, turn it off once:
'steps' => ['watchdog' => ['enabled' => FALSE]],
That beats tagging every feature file with @behat-steps-skip:WatchdogTrait.
A trait's hooks run around every scenario or step. To switch off every hook of a trait for 1 scenario or feature, add this tag:
@behat-steps-skip:TRAIT
TRAIT is the name of the trait, as in @behat-steps-skip:EmailTrait, and the tag works on the Feature: line as well as the Scenario: line. A skip tag whose value isn't shaped like a trait name, such as a hook method, stops the whole run before the scenario starts. So does a skip tag naming a trait that none of your suite's contexts compose, such as a misspelled one.
WatchdogTrait is the check you're most likely to meet first, because DrupalContext composes it. It reads the errors each scenario logged from the watchdog table, which only the core dblog module creates. It reads that table through LogCapabilityInterface, so it also needs a backend that provides that capability. Both drupal and drush do: the in-process backend queries the table, and the Drush backend runs watchdog:show.
Those are the trait's prerequisites, and it checks them when a scenario starts. Here's what happens with and without dblog:
| Watchdog check | dblog enabled | dblog not enabled |
|---|---|---|
| On, the default | Reads the errors after the last step, and fails a scenario that logged one | Fails the scenario at its start, before any step runs |
Off, through watchdog.enabled or @behat-steps-skip:WatchdogTrait | Reads nothing and bootstraps no backend | Reads nothing and bootstraps no backend |
Under a profile that lists no backend providing LogCapabilityInterface, such as 'backends' => ['blackbox'], the scenario fails at its start the same way. The error names the prerequisite that doesn't hold and both fixes: meet it, or switch the trait off with watchdog.enabled or the skip tag. A scenario that uninstalls dblog itself starts with the table in place, so it fails at its last step instead.
Setting fail_on_errors to FALSE or tagging the scenario @error won't help with an unmet prerequisite. Both decide what happens to errors that were read, and without the table or a backend to read it through, there's nothing to read.
Entities a scenario creates
Automatic cleanup
The context registers every entity a scenario creates through a creation step or an entityLifecycleCreate*() helper. At the end of the scenario, it deletes them in reverse creation order, so a node goes before the term it references.
Register what it creates covers a step of your own that creates an entity itself.
To keep all entities after a scenario, add @behat-steps-skip:EntityLifecycleTrait to the scenario or feature. @behat-steps-skip:AuthTrait does the same for users and roles.
To keep only the entities of 1 type, add @behat-steps-entity-cleanup-skip:ENTITY_TYPE_ID, such as @behat-steps-entity-cleanup-skip:media. Repeat the tag to keep several types.
To keep what a whole run creates, set BEHAT_STEPS_DISABLE_CLEANUP, as Environment variables describes.
Several entities with the same title
When several entities share the title, label, name or description a step names, the step acts on the newest one. If your scenario created 1 of them, that's the one it acts on, even on a site that already holds a page called "About us". If it created none, the step acts on the newest of the entities already there.
- Nodes, terms, media, content blocks, menu links and ECK entities are compared by ID, which only grows when an entity is created, so the highest ID wins. Saving an older entity again doesn't make it newer.
- Blocks and menus have machine names rather than numeric IDs, so the one the scenario created last wins. When the scenario created none of them, the machine name that comes last in natural order wins, so
block_10beatsblock_2. - A
... does not existstep removes every match, and a step that only checks whether an entity exists, such asthe :content_type content with the title :title should not exist, looks at every match.
Where to next
For a value no option reaches, Go further than the options shows how to override the trait's own seam in your context, and the rest of Extending the step vocabulary covers steps of your own.