This library is a collection of reusable testing actions and assertions for automated testing with Behat. It's designed to help you write reliable, readable, and maintainable tests faster.
We provide a set of generic traits that can be used in any PHP project, with special support for Drupal through additional step definitions. Every method a trait contributes starts with the trait's own name, so it won't clash with the methods of your existing custom step definitions.
Our goal is to make this a go-to library for Behat steps. We maintain solid test coverage to avoid false positives and negatives, and follow clear guidelines to keep the step language consistent.
We actively maintain this package and welcome contributions from the community.
- π§© 57 step traits: 28 that work on any website, and 29 that only mean something on a Drupal site.
- π§° Every step is a thin wrapper over named helpers, so a project's own domain steps call the same code. HELPERS.md lists the public ones.
- π A step names the capability it needs rather than a backend, so a step whose capability Drush provides, such as clearing caches or creating users, runs through Drush as well as in-process.
- β Behat 3.33+ and Behat 4, Drupal 11 and 12, and PHP 8.3 to 8.5.
- π BrowserKit, Selenium and Chrome DevTools browser drivers.
- π§Ή Entities a scenario creates through its steps are deleted after it, in reverse creation order.
- π AI-maintained architecture documentation in docs/architecture/ via the
update-architecture-docsskill.
| Version | Behat | Drupal | PHP | Support |
|---|---|---|---|---|
| 4.x | 3.33+, 4 | 11, 12 | 8.3, 8.4, 8.5 | The current major. Receives new features, bugfixes and security updates. |
| 3.x | 3 | 10, 11 | 8.2, 8.3, 8.4, 8.5 | The current minor is LTS until 1 July 2027 and receives bugfixes and security updates only. |
| 2.x | 3 | 9, 10 | 8.2+ | Unsupported |
MIGRATION.md takes a project from 3.14 to 4.0, in the order an upgrade meets each change. An AI coding agent can apply it for you with the migrate-behat-steps skill: it reads the guide, plans every change the guide calls for in your project, and asks before it changes anything. Once you say yes, it applies the plan, runs your checks and reports what's left for you.
Composer installs this package without the repository's .claude/ directory, so the skill isn't in vendor/. Paste this into your agent from your project's root, on a clean branch, so git diff shows the whole migration:
Fetch
https://raw.githubusercontent.com/drevops/behat-steps/main/.claude/skills/migrate-behat-steps/SKILL.md
and follow it to migrate this project from drevops/behat-steps 3.x to 4.x.
If you support skills, save it as .claude/skills/migrate-behat-steps/SKILL.md
first, so the migration can run again.
- π STEPS.md - the vocabulary: every step, with an example for each.
- π§° HELPERS.md - the toolbox: every helper the steps are built on, which your own step definitions call the same way.
- π§± Usage - the extension-plus-context model: composing a context, registering it, configuring the traits it holds and switching their hooks off, and what happens to the entities a scenario creates.
- π§ Configuration - the 4 channels a project configures this package through, and the suite layout to start from.
- π Scenario styles - the imperative and declarative scenario styles, the job each one does, and how to graduate from the shipped steps to your own domain steps built on the same helpers.
- π HTTP clients - the 3 clients a step sends its own requests through, where their settings come from, and how to change them.
- π Architecture - how the layers fit together and how a step travels from a feature file to a browser or a Drupal API, with UML diagrams.
- π€ CONTRIBUTING.md - conventions, layers and the local development setup.
composer require --dev drevops/behat-steps:^4With minimum-stability: stable, Composer's default, that installs Behat 3. Behat 4 needs friends-of-behat/mink-extension 3, which has only an alpha release so far, and Composer takes a stability flag such as @alpha only from your own composer.json. To install on Behat 4, ask for both:
composer require --dev drevops/behat-steps:^4 behat/behat:^4 \
friends-of-behat/mink-extension:^3.0@alphaA project whose minimum-stability already admits alpha releases can get Behat 4 from the first command, but nothing in it pins the major. Require behat/behat:^4, as above, when the suite needs Behat 4.
This package replaces drupal/drupal-extension and drupal/drupal-driver and conflicts with both, so Composer won't install it while your project still requires either one. The migration guide shows how to remove them.
To keep installs lean, packages needed by only some traits are declared as suggest rather than hard requirements (the hard requirements are Behat, Mink with its BrowserKit driver and extension, and a set of Symfony components). Add the ones for the traits you use to your project's require-dev - run composer suggests to list them:
JsonTraitneedssoftcreatr/jsonpathfor JSON path steps andjustinrainbow/json-schemafor JSON schema steps.@javascriptscenarios need a JavaScript-capable browser driver - see JavaScript browser drivers below.
Steps that require a real browser (used by scenarios tagged @javascript) work with the 2 browser drivers this library ships adapters for: the Selenium/WebDriver browser driver from lullabot/mink-selenium2-driver, or the Selenium-less dmore/chrome-mink-driver, which talks to Chrome directly over the Chrome DevTools Protocol. Both are exercised by this library's own CI. Another browser driver needs an adapter registered for it.
To run @javascript scenarios without a Selenium server, add dmore/behat-chrome-extension (which pulls in dmore/chrome-mink-driver) and point it at a headless Chrome. It accepts Behat 3 and, from 1.5.0, Behat 4, and this library's own CI runs it on both.
use Behat\Config\Extension;
use Behat\MinkExtension\ServiceContainer\MinkExtension;
use DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension;
$profile
->withExtension(new Extension(ChromeExtension::class))
->withExtension(new Extension(MinkExtension::class, [
'browser_name' => 'chrome',
'javascript_session' => 'chrome',
'sessions' => [
'chrome' => ['chrome' => ['api_url' => 'http://chrome:9222']],
],
]));A profile holds 1 entry per extension, so a profile that already registers MinkExtension merges these settings into that entry rather than adding a second one.
Any image that exposes a DevTools endpoint works (for example chromedp/headless-shell). For local visual debugging you can override api_url at runtime via Behat's BEHAT_PARAMS environment variable - for instance to drive a headed Chrome on your host - as long as that browser can reach your site's base_url.
The vocabulary sits on 1 chain. WebContext carries every step that drives a page, and DrupalContext extends it with every step that reaches a Drupal site, so a suite registers exactly one of them:
$suite = (new Suite('default'))
->withPaths('%paths.base%/tests/behat/features')
->addContext(DrupalContext::class)
->addContext(MinkContext::class);Registering both is fatal, because DrupalContext already carries the 28 web traits and each of their steps would register twice. MinkContext is Mink's own context: it sits beside either one and brings the base browser steps, such as I fill in and I press.
That needs no PHP of your own. To pick your own traits instead, extend the root context and compose them:
<?php
use DrevOps\BehatSteps\WebRawContext;
use DrevOps\BehatSteps\Steps\Web\CookieTrait;
/**
* Defines application features from the specific context.
*/
class FeatureContext extends WebRawContext {
use CookieTrait;
}WebRawContext registers no steps of its own: it owns the backend access, the configuration and 3 of the web helper traits. A step trait brings the helper traits it needs, so composing ContentTrait also brings the entity teardown that removes what its steps created. Usage covers the 3 entry points and the rules that govern composing them.
Ensure that your behat.php enables the extension:
<?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\DrupalContext;
use DrevOps\BehatSteps\Extension\ServiceContainer\BehatStepsExtension;
$suite = (new Suite('default'))
->withPaths('%paths.base%/tests/behat/features')
->addContext(DrupalContext::class)
->addContext(MinkContext::class);
$profile = (new Profile('default'))
->withSuite($suite)
->withExtension(new Extension(BehatStepsExtension::class, [
'backends' => ['drupal', 'blackbox'],
'drupal' => ['drupal_root' => 'web'],
'steps' => ['message' => ['selectors' => [
'success' => '.messages.messages--status',
]]],
]));
return (new Config())->withProfile($profile);The steps entry gives the message steps the selector of the success message, which the scenario below asserts on. Message selectors have no default.
The backends list says which backends a scenario may reach, and in what order. A step never names a backend - it names the capability it needs, and the first backend in the list providing that capability answers. See Backend resolution.
Browser sessions come from Mink's own extension, Behat\MinkExtension\ServiceContainer\MinkExtension, registered in the same profile as behat.dist.php shows. BehatStepsExtension reads the http_client_parameters a browserkit_http session declares and sends the requests steps make from PHP, such as a file download, with the same options. So the page and the download reach the site with the same settings, and HTTP clients covers the rest.
By default, DrupalContext fails any scenario that logs a PHP error, reading those errors from the table the core dblog module creates, through a backend that loads Drupal into the Behat process. On a site without dblog, or under a profile without such a backend, every scenario fails at its start until you provide what's missing or switch the Watchdog check off.
Behat 4 reads only PHP configuration, from behat.php or, when there is no behat.php, from behat.dist.php. Behat 3 also accepts the same settings in behat.yml.
behat.dist.php sets every extension option, plus the trait options a project almost always sets, as a reference, and docs/configuration.md documents all 4 configuration channels, including the suite layout to start from.
The imperative style spells the interaction out and needs no PHP of your own, so the suite covers a flow the moment it is written:
Scenario: Editor publishes a page
Given I am logged in as a user with the role "editor"
When I visit "/node/add/page"
And I fill in "Title" with "About us"
And I press "Save"
Then the path should be "/about-us"
And I should see the success message "has been created"The declarative style names the behavior instead of the mechanics:
Scenario: Editor publishes a page
Given I am an editor
When I publish a page titled "About us"
Then the page "About us" should be publicly visible at "/about-us"No shipped step matches those lines. You write them yourself, over the same helpers the shipped steps are built on:
#[When('I publish a page titled :title')]
public function publishPage(string $title): void {
$this->entityLifecycleCreateNode(new EntityStub('node', 'page', [
'title' => $title,
'moderation_state' => 'published',
]));
}That is the lifecycle this package is built for: start in the vocabulary for coverage on day one, then graduate the flows that matter into domain steps on the toolbox. The scenarios change language; the code underneath them does not.
Scenario styles explains when each style earns its keep, and HELPERS.md lists every helper a domain step can build on.
| Class | Description |
|---|---|
| AccessibilityTrait | Assess accessibility of rendered pages. |
| BasicAuthTrait | Keep HTTP basic authentication applied across session resets. |
| CommandTrait | Run local shell commands and assert on their result. |
| CookieTrait | Verify and inspect browser cookies. |
| DateTrait | Convert relative date expressions into timestamps or formatted dates. |
| DiagnosticsTrait | Append on-failure diagnostics to the failure message of any failed step. |
| DropzoneTrait | Simulate a real multi-file drag-and-drop gesture onto a Dropzone target. |
| ElementTrait | Interact with HTML elements using CSS selectors and DOM attributes. |
| FieldTrait | Manipulate form fields and verify widget functionality. |
| FileDownloadTrait | Test file download functionality with content verification. |
| IframeTrait | Switch between iframes and the root document. |
| JavascriptTrait | Automatically detect JavaScript errors during test execution. |
| JsonTrait | Assert JSON responses with path and schema checks. |
| KeyboardTrait | Simulate keyboard interactions in the browser. |
| LinkTrait | Verify link elements with attribute and content assertions. |
| MappingTrait | Replace {{ Key }} tokens in step arguments and table cells. |
| MessageTrait | Assert status, error, warning and success messages rendered on the page. |
| MetatagTrait | Assert <meta> tags and head/SEO markup in page markup. |
| ModalTrait | Interact with and assert modals. |
| PathTrait | Navigate and verify paths with URL validation. |
| RandomTrait | Replace random-value tokens in step arguments and table cells. |
| RegionTrait | Interact with and assert against named page regions. |
| ResponseTrait | Verify HTTP response headers. |
| ResponsiveTrait | Test responsive layouts with viewport control. |
| RestTrait | Test REST APIs with lightweight steps. |
| TableTrait | Interact with HTML table elements and assert their content. |
| WaitTrait | Wait for a period of time or for AJAX to finish. |
| XmlTrait | Assert XML responses with element and attribute checks. |
| Class | Description |
|---|---|
| Drupal\BatchTrait | Wait for Drupal's Batch API to finish. |
| Drupal\BigPipeTrait | Wait for Drupal BigPipe placeholders to be replaced on JavaScript scenarios. |
| Drupal\BlockTrait | Manage Drupal blocks. |
| Drupal\CacheTrait | Invalidate Drupal caches and run cron from within a scenario. |
| Drupal\ConfigOverrideTrait | Disable Drupal config overrides from settings.php during a scenario. |
| Drupal\ConfigTrait | Assert and set stored Drupal configuration values with automatic revert. |
| Drupal\ContentBlockTrait | Manage Drupal content blocks. |
| Drupal\ContentTrait | Manage Drupal content with workflow and moderation support. |
| Drupal\DraggableviewsTrait | Order items in the Drupal Draggable Views. |
| Drupal\DrushTrait | Run Drush commands and assert their output. |
| Drupal\EckTrait | Manage Drupal ECK entities of an existing entity type and bundle. |
| Drupal\EmailTrait | Test Drupal email functionality with content verification. |
| Drupal\EntityTrait | Create entities of a type that has no dedicated trait. |
| Drupal\FileTrait | Manage Drupal file entities with upload and storage operations. |
| Drupal\LanguageTrait | Create the languages a scenario needs. |
| Drupal\MediaTrait | Manage Drupal media entities with type-specific field handling. |
| Drupal\MenuTrait | Manage Drupal menus and menu links. |
| Drupal\ModuleTrait | Enable and disable Drupal modules with automatic state restoration. |
| Drupal\ParagraphsTrait | Manage Drupal paragraphs entities with structured field data. |
| Drupal\QueueTrait | Manage and assert Drupal queue state. |
| Drupal\RedirectTrait | Manage Drupal redirect entities provided by the contrib redirect module. |
| Drupal\SearchApiTrait | Run Drupal Search API indexing and cron hooks. |
| Drupal\StateTrait | Manage and assert Drupal State API values with automatic revert. |
| Drupal\TaxonomyTrait | Manage Drupal taxonomy terms with vocabulary organization. |
| Drupal\TestmodeTrait | Configure Drupal Testmode module for controlled testing scenarios. |
| Drupal\TimeTrait | Control system time in tests using Drupal state overrides. |
| Drupal\UserTrait | Manage Drupal users with role and permission assignments. |
| Drupal\WatchdogTrait | Assert Drupal does not trigger PHP errors during scenarios using Watchdog. |
| Drupal\WebformTrait | Manage Drupal webforms. |
This package follows semantic versioning, and 5 surfaces are covered by it. A breaking change to any of them waits for a major release:
- Step text - the pattern a scenario matches, listed in STEPS.md.
- Helpers - every public method the step traits, the helper traits and
WebRawContextcontribute that is not a step, a hook or a transform, listed in HELPERS.md. They sit on$thisin your own context, so a domain step depends on them exactly as a scenario depends on step text. Aprotectedmethod is an implementation detail and carries no such promise. - Configuration - the options under the
behat_stepskey and the tags, listed in docs/configuration.md, along with the contracts a class those options name implements:FieldHandlerInterfacefordrupal.field_handlers, andPreCreateAliasInterfaceorPostCreateAliasInterfacefordrupal.creation_aliasesanddrush.creation_aliases. - Exceptions - which exception type a failure reports, listed under Exceptions below.
- Context base classes -
WebRawContext,WebContextandDrupalContext, which a project extends.
2 things sit outside it: any member whose docblock carries @internal, and the internals of src/Backend and src/Extension that none of the surfaces above exposes.
A project adds or replaces a field handler through the drupal.field_handlers option, or from a context through FieldHandlerCapabilityInterface, and a creation alias through the drupal.creation_aliases and drush.creation_aliases options, or from a context through CreationAliasCapabilityInterface, rather than a subclass of Core or a backend: a subclass of Core, a backend or a field handler relies on protected methods a minor release can rename or stop calling.
This library reports failures with a small, fixed set of exception types, mostly Mink's:
| Exception | When thrown |
|---|---|
ElementNotFoundException |
An element on the page or in an XML response is not found |
ExpectationException |
Assertion fails (value mismatch, state verification) |
AssertionException |
Assertion fails in a step with no Mink session |
UnsupportedDriverActionException |
The browser driver lacks a capability the step needs |
UnsupportedBackendActionException |
No backend the scenario lists provides a capability the step needs |
\RuntimeException |
Invalid input or processing error (not an assertion) |
ElementNotFoundException extends ExpectationException, so catching ExpectationException covers both. An attribute, a JSON path or a table column isn't an element, so a missing one throws ExpectationException.
DrevOps\BehatSteps\Steps\Exception\AssertionException is thrown by traits that never touch the browser, such as Steps\Web\CommandTrait and Steps\Drupal\ConfigTrait. ExpectationException needs a browser driver, which those traits do not have, so they report a failed assertion with this instead.
Example error messages:
Element matching css "#my-element" not found.
Link with title "My Link" not found.
Select with id|name|label "My Select" not found.
The cookie "session" was not set.
Copy and paste below into your project's CLAUDE.md or AGENTS.md file.
## Writing Behat Tests
Available step definitions are listed in `.behat-steps.txt`.
Read this file before writing or modifying Behat tests.
Use only step patterns from this file. Do not invent steps.
If `.behat-steps.txt` does not exist or is outdated, regenerate it:
./vendor/bin/behat --definitions=i > .behat-steps.txt
Regenerate after adding new step traits or updating dependencies.
For detailed examples, see: vendor/drevops/behat-steps/STEPS.md
See CONTRIBUTING.md for local development setup and the linting and testing commands.
This repository was created using the Scaffold project template
