This repository contains Behat step definitions for PHP projects (with specialized support for Drupal), organized as traits that can be included in Behat contexts. The steps provide reusable testing functionality for generic PHP components and Drupal-specific features.
Source files are located in the src directory. Each trait is organized into a separate file, and the steps are defined within those files.
composer require --dev drevops/behat-steps:^3Note: These commands are for developing this Behat steps library itself, not for using it in your project.
ahoy build- Setup a fixture Drupal site in thebuilddirectoryahoy test-bdd- Run all BDD testsahoy test-bdd path/to/file- Run all scenarios in specific feature fileahoy test-bdd -- --tags=wip- Run all scenarios tagged with@wiptagahoy debug- Enable debuggingahoy drush cex -y- Export configurationahoy update-fixtures- Update fixture files for Drupal from the current buildahoy copy-files- Update fixture files to the current buildahoy update-docs- Update documentationahoy lint-docs- Check documentation for errorsahoy lint- Run linting (if docker-compose.yml exists, otherwise usecomposer lint)ahoy lint-fix- Fix linting issues (if docker-compose.yml exists, otherwise usecomposer lint-fix)ahoy test-unit- Run unit tests (if docker-compose.yml exists, otherwise usecomposer test)
CRITICAL: Always copy fixture files to all three locations
When creating or updating fixture files in tests/behat/fixtures/, you MUST
immediately copy them to build/web/sites/default/files/
Example:
cp tests/behat/fixtures/example.xml build/web/sites/default/files/example.xml-
General Guidelines:
- Use tuple format instead of regular expressions
- Use descriptive placeholder names
- Use
the followingfor tabled content - Use
withfor properties:Then the link with the title :title should exist - Avoid optional words like
(the|a) - Omit unnecessary suffixes like
on the page - Method names should begin with the trait name:
userAssertHasRoles()
-
Given Steps:
- Define test prerequisites
- Use words like
existsorhave - Avoid using
shouldorshould not - Avoid using
Given I
-
When Steps:
- Describe an action with an action verb
- Use the format
When I <verb>
-
Then Steps:
- Specify assertions and expectations
- Use
shouldandshould notfor assertions - Start with the entity being asserted
- Avoid using
Then I - Methods should include the
Assertprefix
-
Block assertions:
I should see the block with label "..."I should see the block with label "..." in the region "..."
-
Content block operations:
the content block type "..." should existthe following "..." content blocks exist:I edit the "..." content block with the description "..."
-
Email testing:
I enable the test email systemI clear the test email system queuean email should be sent to the "..."
Some traits provide beforeScenario hook implementations that can be disabled by adding behat-steps-skip:METHOD_NAME tag to your test.
Example: To skip beforeScenario hook from ElementTrait, add @behat-steps-skip:ElementTrait tag to the feature.
- Code is written using Drupal coding standards
- Local variables and method arguments:
snake_case - Method names and class properties:
camelCase
- List of all available steps is produced from trait and method comments and exported into STEPS.md
The STEPS.md documentation is automatically generated from the source code using the docs.php file. After making changes to step definitions or adding new ones, you should regenerate the documentation:
-
Using Ahoy (recommended):
ahoy update-docs
-
Direct PHP execution:
php docs.php > STEPS.md -
Linting the documentation:
ahoy lint-docs
This ensures that the documentation remains in sync with the actual code implementation.
- Drupal compound fields (like datetime, daterange) have multiple sub-inputs that cannot be targeted with standard
findField() - Use XPath-based selectors to locate specific sub-inputs within compound fields
- Implement fallback strategies: try label elements first, then span elements, then generic class-based searches
- Compound field structure example:
field_name[0][value][date]andfield_name[0][value][time] - Date range fields use
[value]for start and[end_value]for end components
- New field configurations must be added to both d10 and d11 fixtures
- Field configs include: field storage, field instance, and form display updates
- When adding fields, update
core.entity_form_display.node.page.default.ymlwith:- Field references in dependencies config section
- Module dependencies (e.g.,
datetime,datetime_range) - Widget configuration with type, weight, region, and settings
- After creating configs in
build/config/sync, copy to both fixture directories - Use
ahoy drush cim -yto import configurations into the build environment
- Consolidate related tests into existing feature files rather than creating new ones
- Use descriptive tags (e.g.,
@datetime) to allow selective test execution - Negative tests using
@trait:FieldTraitshould use simple navigation (e.g.,I go to "node/add/page") - Avoid using custom steps in negative tests that may not be available in BehatCLI context
- Documentation tool (
docs.php) does not support multiple@Whenannotations per method - Use a single step annotation and document alternative usage in
@codeexamples - Optional parameters should use empty string defaults, not PHP optional parameters
- Always provide both imperative (content) and continuous (activeForm) task descriptions
When writing @trait scenarios that test BehatCliContext functionality (tests that run Behat within Behat), nested PyStrings are required when the inner scenario steps themselves accept PyString arguments.
Problem: Standard escaped PyString delimiters \"\"\" don't work because:
- Gherkin parser captures the outer PyString as literal text including the escaped quotes
- BehatCliContext writes this literal text to a generated feature file
- Gherkin parser fails when parsing the generated file with malformed PyString syntax
Solution: Use triple single quotes ''' for inner PyStrings:
@trait:SomeTrait
Scenario: Test error condition
Given some behat configuration
And scenario steps tagged with "@api @email":
"""
When I send test email to "test@example.com" with:
'''
Email body content here
'''
Then an email should be sent
"""How it works: BehatCliTrait.php:203 converts ''' → """ after extracting the PyString but before writing the generated feature file, ensuring proper Gherkin syntax.
Example test: See tests/behat/features/behatcli.feature:131 for a demonstration of nested PyStrings.
CRITICAL: When running ahoy test-bdd-coverage <path>, TWO separate cobertura.xml files are generated:
-
.logs/coverage/behat/cobertura.xml- Contains coverage from @api tests only (direct execution)
- Shows what regular Behat scenarios cover
- Example: EmailTrait shows 83.63%
-
.logs/coverage/behat_cli/cobertura.xml- Contains MERGED coverage (API tests + @trait subprocess tests)
- This is the TRUE total coverage to report
- Example: EmailTrait shows 90.06% (correctly higher)
How it works:
- During test execution, @trait scenarios spawn subprocess Behat runs
- Each subprocess generates a coverage file in
.logs/coverage/behat_cli/phpcov/*.php - After tests complete,
scripts/merge-coverage.phpmerges all subprocess coverage with the main behat coverage - The merged result is written to
behat_cli/cobertura.xml
Important: The ahoy test-bdd-coverage command automatically cleans up old subprocess coverage files before running tests to prevent pollution from stale data. Always check the behat_cli/cobertura.xml (merged) file for the accurate total coverage percentage.
Assessing coverage after running tests:
RECOMMENDED: Use the scripts/check-coverage.php script for easy coverage assessment:
# Run tests with coverage
ahoy test-bdd-coverage tests/behat/features/some_feature.feature
# Check coverage using the script (uses MERGED coverage by default)
php scripts/check-coverage.php SomeTrait
# Output shows:
# Class: DrevOps\BehatSteps\SomeTrait
# Line rate: 0.95901639344262 (95.90%)
#
# Uncovered lines:
# 47, 50, 210, 259, 491Manual method (if script is not available):
# Run tests with coverage
ahoy test-bdd-coverage tests/behat/features/some_feature.feature
# Check API-only coverage
grep 'class name="DrevOps\\BehatSteps\\SomeTrait"' .logs/coverage/behat/cobertura.xml | grep -o 'line-rate="[^"]*"'
# Check MERGED coverage (THIS IS THE TRUE COVERAGE)
grep 'class name="DrevOps\\BehatSteps\\SomeTrait"' .logs/coverage/behat_cli/cobertura.xml | grep -o 'line-rate="[^"]*"'Coverage Check Script Usage:
# Check coverage for a trait (uses merged coverage by default)
php scripts/check-coverage.php <TraitName>
# Check coverage using a specific coverage file
php scripts/check-coverage.php <TraitName> <path/to/cobertura.xml>
# Examples:
php scripts/check-coverage.php ElementTrait
php scripts/check-coverage.php ResponsiveTrait .logs/coverage/behat/cobertura.xmlIMPORTANT: Always use php scripts/check-coverage.php when you need to assess coverage for a trait. This script:
- Automatically uses the correct merged coverage file (
.logs/coverage/behat_cli/cobertura.xml) - Shows the line rate as both decimal and percentage
- Lists all uncovered line numbers
- Handles both Docker and local path conventions
- Use descriptive field names without "test" prefix (e.g.,
field_datetimenotfield_test_datetime) - Field labels should be user-friendly: "Event date", "Event period", etc.
- Machine names follow Drupal conventions:
field_{description}
- Always run
ahoy update-docsafter adding/modifying step definitions - Use
ahoy lint-docsto verify documentation format - Run
ahoy lintto ensure code passes all quality checks (PHPStan, Rector, Gherkinlint) - Run BDD tests with specific tags during development:
ahoy test-bdd -- --tags="@tagname"
IMPORTANT: Never Modify Code Quality Tool Configurations
- NEVER modify
phpstan.neon,phpcs.xml,rector.php, or other code quality tool configs to make lint pass - If linting errors occur, fix the actual code to address the issues
- Do not add ignores or suppressions to configuration files
- If errors seem legitimate and can't be fixed, ask for guidance instead
- Code quality standards must be maintained consistently across the project