Background Steps Silently Corrupt Isolation
Teams running Cucumber-JVM 7 or Behave at scale almost always have the same archaeology problem: a Background: block that started as three lines in 2019 and now spans twenty, half of which nobody can explain. The steps compile, the suite is green, and yet flaky failures appear on parallel runs in ways that are genuinely hard to reproduce locally. The Background isn't the only culprit, but it's the most systematically overlooked one.
The core issue is that Background steps execute before every scenario in a feature file, sharing mutable state across what are supposed to be independent test cases. When that state isn't fully reset — or when reset order matters — scenarios start depending on each other's side effects. The failure mode is subtle: tests pass in isolation, fail in suite, and produce different results depending on execution order or thread count.
By the end of this article you'll be able to identify the three structural patterns that cause Background-driven isolation failures, instrument your suite to detect them before they hit CI, and refactor without breaking your scenario count or readability. This matters now because Playwright's parallel worker model and Cucumber-JVM 7's concurrent step execution have made the window between "technically works" and "reliably green" much narrower.
Clear explanations for everyday frustrations involving work, money, technology, health, and relationships.
What Background Actually Does to Your Execution Model
A Background: block is syntactic sugar for a before-hook that runs as Gherkin steps — visible in reports, parsed by the same step-definition registry, and subject to the same glue-code bindings. In Cucumber-JVM 7 and SpecFlow 3+, each scenario gets its own world object (or ScenarioContext), but the Background steps execute within that world before the scenario's own steps. The distinction matters: the world is fresh, but if a Background step writes to an external resource — a database row, a Redis key, a browser cookie — that resource is shared infrastructure, not a per-scenario sandbox.
Where this fits in a modern test architecture: Background is appropriate for pure configuration (setting a base URL, selecting a tenant, loading a fixture file from disk). It is not appropriate for anything that touches a stateful external system without a guaranteed, ordered teardown. The moment a Background step calls an API, inserts a row, or authenticates a session token that gets cached server-side, you've introduced a shared-state dependency that your scenario isolation model cannot see — and your test runner will not warn you about it.
Three Failure Patterns and How to Instrument Them
The most common pattern is cumulative database state. A Background step inserts a user record before each scenario, but the teardown hook runs after the entire feature rather than after each scenario. In a sequential run this is invisible — the second scenario's insert fails with a unique-constraint violation and the test is marked as a setup error, not a logic failure. In a parallel run with Cucumber-JVM 7's --parallel flag, two workers hit the same table simultaneously and you get a race, not a constraint error. The failure message points at the assertion, not the Background.
# Bad: Background writes to shared DB, no per-scenario teardown
Feature: User preferences
Background:
Given a user "alice@example.com" exists in the database
And the user is logged in
Scenario: Update display name
When alice updates her display name to "Alice A."
Then the profile page shows "Alice A."
Scenario: Reset preferences
When alice resets all preferences
Then the display name is "alice@example.com"
Running this with cucumber --parallel 2 means both scenarios share the same "alice" row if your @After hook deletes it at feature scope instead of scenario scope. The fix is mechanical: move teardown to @After(order=100) at scenario scope in your hooks file, and make the Background step idempotent with an upsert rather than a raw insert.
# hooks.py (Behave)
import behave
@behave.fixture
def db_user(context):
context.user = upsert_user("alice@example.com")
yield
delete_user(context.user.id) # runs after EACH scenario
def before_scenario(context, scenario):
behave.use_fixture(db_user, context)
The second pattern is browser session leakage. A Background step navigates to a login page and authenticates. Playwright's browser context is per-worker, not per-scenario, unless you explicitly call browser.new_context() in a before-scenario hook. With Cypress 13's experimentalMemoryManagement enabled, session state persists across tests in the same spec file by design — which means your Background login step is a no-op for scenarios 2–N, but your assertions still assume a clean auth state. The symptom is scenarios passing individually and failing when the spec file runs in full. Instrument this by logging context.storageState() at the start of each scenario and diffing against a known-clean baseline; a 10-line script in a beforeEach hook will surface the leak within one run. For a deeper look at how Playwright and Selenium differ in their context isolation models, the trade-offs are more significant than most teams expect.
The third pattern is step-definition registry collision, which is less about state and more about which glue code actually runs. When Background steps are defined in a shared library and overridden in a feature-specific steps file, load order determines which binding wins — and load order is not guaranteed in Behave or Cucumber-JVM when you use wildcard imports. The result is a Background step that silently calls the wrong implementation depending on which feature file loads first. This is especially pernicious in monorepos where step definition registries fragment across shared libraries without a clear ownership model. The fix: explicit imports, never wildcards, and a CI job that runs your suite in reverse alphabetical feature-file order to smoke out ordering dependencies.
# pytest-bdd: explicit step import, no wildcard
# conftest.py
from steps.auth_steps import given_user_exists # explicit
from steps.auth_steps import given_user_logged_in # explicit
# NOT: from steps import *
Why This System Feels Slow, Rigid, or Frustrating
The most persistent mistake is treating Background: as a before-hook with better readability, then skipping the teardown discipline that proper hooks enforce. Before-hooks in Cucumber-JVM have a well-defined order parameter; Background steps have none. When two Background steps each set up a different part of the same resource — one sets a feature flag, another sets a user role — and a scenario changes the flag mid-test, the next scenario inherits the mutated flag because the Background step only sets it if it isn't already set. This is an org-level failure as much as a tooling one: Background steps get reviewed as documentation, not as code with side effects.
The second mistake is assuming that tagging a scenario @isolated and running it alone proves it's clean. Isolation failures are relational — they only manifest when scenario A precedes scenario B in the same worker. A single-scenario run will always pass. The correct instrument is a randomized execution order job: add --order random --seed $RANDOM to your Behave or Pytest-BDD invocation in CI, log the seed, and treat any seed-dependent failure as a P1 isolation bug. GitHub Actions makes this trivial with a matrix strategy over a handful of fixed seeds.
Myths That Keep Background Debt Accumulating
Myth 1: "If the suite is green in CI, isolation is fine." CI typically runs scenarios sequentially in a single thread. Isolation failures are a parallelism problem. A suite that's been green for two years can fail on the first day you add --parallel 4 to cut runtime. Myth 2: "Background steps are safer than hooks because they're visible." Visibility in the Gherkin report doesn't make them safer — it makes their failures more confusing, because a failing Background step is reported as a scenario failure, not a setup failure, which misleads triage. Myth 3: "Refactoring Background into hooks loses traceability." This is the most damaging myth. Hooks can emit structured log lines or OpenTelemetry spans that are far more queryable than Gherkin report output. If you're on a high-velocity team doing risk-based triage, structured hook telemetry beats Gherkin prose for root-cause speed.
The underlying mental model error is conflating readability with correctness. Background steps are a communication tool; they were never designed to be a state-management primitive. Teams that treat them as both end up with feature files that are readable in isolation and unreliable in aggregate. The fix isn't to abolish Background — it's to enforce a hard rule: Background steps may only read or configure; they may never write to shared infrastructure without a guaranteed, scenario-scoped teardown. Lint this. A grep for database write patterns in Background step bindings, run as a pre-commit hook, costs thirty minutes to set up and catches the class of bug permanently.
Background isolation debt compounds quietly — it's the kind of problem that doubles your flaky-test rate before anyone names it. Start by auditing every Background step that touches external state, instrument your CI with a randomized-seed parallel run, and enforce the read-only rule via a pre-commit lint check. Once isolation is clean, the next metric worth tracking is mean-time-to-detect on order-dependent failures: if your randomized-seed job catches them within one PR cycle, your feedback loop is working.
Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.