Debugging Flow
Start with the cheapest signal and escalate. Most failures resolve at steps 1-3.
- Read the failure output and
var/log/<env>.log— Behat surfaces "element not found" for what is often a 500 in the app. The real cause lives in the server log, not the Behat stack trace. - Re-run with
-v/-vv/-vvv— verbose shows the matched step definition, very verbose adds hook execution, debug adds the full step matcher trace. Escalate one level at a time. - Capture state — insert
And I take screenshotat the suspected failure point. Oro'sScreenshotTrait::takeScreenshotcaptures cursor position too, except when a browser alert is showing. Dropdump($variable)into Context classes to inspect runtime state.- Multi-gate failure? Go breadth-first with ONE diagnostic step, not N sequential runs. When a scenario fails with an opaque "thing missing / empty" symptom and there are several plausible gates (visibility tables + scope + Elasticsearch index + system config + feature flag, etc.), write a single
@Then I dump X for :argstep in the Context that queries EVERY plausible culprit in one shot and throws the combined state as aRuntimeException. One 2–4 minute behat cycle then returns all the evidence at once. Serialised hypothesis-at-a-time runs compound cost and context-switching. Wrap each sub-query in try/catch so a missing table or wrong service name doesn't suppress the rest. Revert the diagnostic step before committing — it's scaffolding, not permanent test coverage. See theComprehensive Visibility/State Dumppattern inreferences/breadth-first-diagnostics.md.
- Multi-gate failure? Go breadth-first with ONE diagnostic step, not N sequential runs. When a scenario fails with an opaque "thing missing / empty" symptom and there are several plausible gates (visibility tables + scope + Elasticsearch index + system config + feature flag, etc.), write a single
- Isolate the scenario — strip every step not strictly required to reproduce. A failing 3-step scenario tells you far more than a failing 30-step one.
- Run with
--stop-on-failure— tight feedback loop when chasing a single failing scenario. - If intermittent: treat it as an AJAX race, not a flake — see
references/ajax-flake.md. AddingAnd I waitis not a fix. - If logic inside a Context is wrong: attach Xdebug — see Xdebug Split-Process below.
Xdebug Split-Process Debugging
This is the part the Oro docs gloss over and it is the highest-leverage technique in the skill.
The Behat runner and the application under test are two separate PHP processes, usually in two containers:
- CLI runner —
bin/behatitself. Runs Context classes, step definitions, element classes, fixtures. Breakpoints in PHP test code fire here. - PHP-FPM — serves the Mink browser's HTTP requests. Breakpoints in controllers, services, repositories, event listeners fire here.
Xdebug on only one of them misses everything on the other. Debugging both requires two targets listening on two ports.
Minimal CLI attach:
XDEBUG_MODE=debug XDEBUG_SESSION=1 php bin/behat path/to/feature.feature
Debugging FPM requires setting the XDEBUG_SESSION cookie inside the Mink browser session so FPM activates its debugger — Mink's Selenium2 driver supports this directly:
$this->getSession()->setCookie('XDEBUG_SESSION', 'IDEKEY');
Put that in a @BeforeScenario hook or a dedicated debug step. Full setup (two listeners, path mapping, container networking, common silent failures) in references/xdebug-split.md.
Step Discovery & Verbosity
Don't guess step wording. List what's actually available:
php bin/behat -dl -s OroUserBundle # names only
php bin/behat -di -s OroUserBundle # names + descriptions + examples
php bin/behat -dl -s OroUserBundle | grep "grid" # filter
Generate skeletons for undefined steps instead of writing them by hand:
php bin/behat path/to/your.feature --dry-run --append-snippets --snippets-type=regex
Tag filtering for focused runs:
php bin/behat --tags=@smoke
php bin/behat --tags="@smoke&&@checkout" # AND
php bin/behat --tags="@smoke,@checkout" # OR
php bin/behat --tags="~@wip" # NOT
Full reference: references/step-discovery.md.
Pausing for Manual Inspection
And I wait for action
Prints "Press [RETURN] to continue..." and blocks until you hit return. Local only — in CI this hangs the job until it times out. Add a pre-commit check (grep for the phrase) if this bites repeatedly.
Key Pitfalls
- Debugging the wrong process. Xdebug on only the CLI runner never hits controller breakpoints; on only FPM it never hits Context breakpoints. Two targets, two ports, two breakpoint sets.
waitForAjaxon non-jQuery requests. The helper only tracks jQuery-registered XHR. Nativefetch()and rawXMLHttpRequestnever enter its queue, so it returns immediately while the request is still in flight. Wait for an observable DOM state, not the AJAX queue.- Committing
And I wait for action. CI blocks forever on the stdin read. Budget a pre-commit hook for it. - Not reading
var/log/<env>.logafter a failure. A server-side exception in a controller surfaces as a frontend "element not found" — Behat can't see the 500, only the missing element that would have rendered on success. - Path-mapping misconfigured in split-process Xdebug. When container paths differ from the IDE's local paths, breakpoints silently don't fire. No error, no warning — the debugger just skips them. Always verify by setting a trivial breakpoint on line 1 of a known-loaded file first.
See Also
references/xdebug-split.md— Full two-process Xdebug setup, cookie propagation, path mapping, silent-failure checklistreferences/step-discovery.md—-dl/-di, snippet generation, tag filtering, verbosity progressionreferences/ajax-flake.md— Race conditions, detached element references, backend-vs-HTTP completion, spin predicate patternreferences/performance-tmpfs.md— PostgreSQL in tmpfs for faster test runs (advanced, not in official docs)references/v6.1.md— 6.1-stable debugging specificsreferences/v7.0.md— 7.x notes (placeholder)- For writing tests: oro-behat-testing skill
- For remote/staging runs: oro-e2e-testing skill
- Upstream: Oro Debug Behat Tests