How do you pass state between the step definitions of one scenario without using globals?
answer
- The steps are separate functions
- Lifetime should equal one scenario
- Anything longer-lived leaks into the next
- Cleanup after a failure never reaches the last step
- Unique data beats elaborate deletion
basics
~20 sPut it in a context object the runner creates fresh for each scenario and hands to every step definition that asks for it. Process-wide or module-level variables survive between scenarios, which creates order dependence and breaks parallel runs.
solid answer
~50 sStep definitions are separate functions, so a Given that creates something and a Then that asserts on it need a channel between them. The right channel is a **scenario-scoped context object** — a small holder created new for each scenario and injected into every step class or module that declares it, so its lifetime is exactly one scenario. Keep it thin: identifiers, handles and the last response, never assertions or driver internals. The wrong channel is a process-wide variable, which outlives the scenario and makes results depend on execution order — and under parallel execution, on timing. Cleanup belongs in an **after hook** rather than a final Then step, because hooks run whether the scenario passed, failed or was aborted mid-way, while a trailing step is skipped after a failure. Better still, prefer per-scenario unique data so there is little to clean up.
code
pseudocode · 20 linesclass ScenarioContext { teacherId = null; lastResponse = null }
class TimetableSteps(context: ScenarioContext) {
step("a teacher named {string} exists", function(name) {
context.teacherId = teachers.create(name + "-" + scenarioToken())
})
step("they request the timetable for period {int}", function(period) {
context.lastResponse = planner.timetableFor(context.teacherId, period)
})
step("the request is refused", function() {
assert(context.lastResponse.status == "REFUSED")
})
}
afterHook(tag = "@creates-teacher", function(context) {
if (context.teacherId != null) { teachers.delete(context.teacherId) }
})go deeper
Be ready to say that steps are separate functions and need a shared holder, and that a variable living longer than one scenario leaks into the next one. Knowing cleanup belongs in a hook is a good extra.
Explain the mechanics: a context object constructed per scenario and injected into the step classes, what belongs in it, and why an after hook runs when a trailing cleanup step would have been skipped after a failure.
Show that you connect isolation to operability — any-order and parallel execution, unique per-scenario data, tagged hooks, reverse-order teardown — and be able to describe a real defect that leaked state hid from the suite.
Own the tradeoff between isolation cost and feedback speed: unique data versus shared fixtures, per-scenario transactions where the architecture allows, and how much environment ownership a team needs before a scenario suite can be trusted.
A scenario is one narrative, but its steps are separate functions with no call relationship. The context step that creates a teacher, the event step that acts, and the outcome step that asserts all need to see the same identifiers. How that channel is built determines whether the suite can be run in any order, in parallel, and repeatedly against a live environment. ## The scenario-scoped context object The standard answer is a small holder object whose lifetime is exactly one scenario. The runner constructs it before the first step and discards it after the last, and it is supplied to each step definition that declares a dependency on it — usually by **constructor injection** into the class or module holding the definitions, so no definition reaches out for it. Because a new instance exists per scenario, nothing leaks between scenarios by construction, and a scenario cannot accidentally rely on a predecessor. Some runners call this the scenario's *world*; the mechanism matters more than the name. ## What belongs in it Keep it thin and typed. - **Keep in:** the identifier of the record the context steps created, the handle or response the event step produced, and the values a later step must assert against. - **Keep out of it:** assertions, driver internals, anything a helper could recompute, and anything that exists only because two definitions were too fat to be split. A context object that has grown thirty fields is usually telling you the definitions have become procedures sharing a scratchpad. Prefer a few purpose-named holders — one for the identities the scenario created, one for the last response — over a single untyped bag of keys, which turns a compile-time question into a run-time one. ## Why process-wide state fails Module-level or process-wide variables outlive the scenario, and three things follow: - **Order dependence:** scenario B passes only when scenario A ran first, so the suite is green as a whole and red when a single scenario is run alone — the fastest way to make people stop trusting it. - **Parallelism:** once scenarios run concurrently, shared mutable state is read and written by unrelated narratives at once, producing failures that look like flakiness. - **And correctness of the assertions themselves:** leftover state can make a scenario pass for reasons it never established. In the school-timetable-planner suite, a context step elevated a teacher to the timetable-editor role and stored the elevated session in a process-wide variable; a later scenario meant to assert that an ordinary teacher *cannot* edit a colleague's timetable reused that session, performed the edit successfully, and reported green. A permission escalation was live in the product and the suite was asserting that it was not. ## Parallelism raises the stakes Scenario suites tend to be the slow tier, and the usual way to hold a time budget — say a 92nd-percentile scenario time of 7.4 seconds — is to run scenarios concurrently. Concurrency is exactly what shared state forbids, so the state design decides whether that lever exists at all. It also pushes isolation past the glue into the data: two concurrent scenarios that both create "the teacher" collide in the database as surely as in a variable. Generating a **unique key per scenario** — a name or identifier salted with a per-scenario token — removes both collisions at once. ## Teardown that leaves nothing behind Two rules carry most of the weight. 1. First, **cleanup goes in an after hook, not in a final Then step**: after a step fails, the remaining steps of that scenario are skipped, so a trailing cleanup step is exactly the code that does not run on the runs where cleanup matters most. Hooks run regardless of outcome. 2. Second, **prefer *not needing* cleanup**: data created fresh and uniquely per scenario, and read-only reference data set up once for the whole run, beat elaborate delete logic. Where cleanup is unavoidable: - make it idempotent and independent of how far the scenario got; - tear down in the reverse order of setup; - and register hooks against tags so that only scenarios that touched an external system pay for its cleanup. State outside your process — files, queues, third-party sandboxes, a mail catcher — never disappears on its own and needs explicit handling; state inside a transaction that the exercised code shares can sometimes be rolled back wholesale, which is the cheapest teardown there is when the architecture allows it. ## The interview signal A strong answer connects the three things: a per-scenario context object gives **isolation**, isolation makes **any-order and parallel execution** possible, and **hook-based teardown plus unique data** keeps the environment clean enough that the isolation survives repeated runs. A weak answer names a global variable and stops, or proposes to fix order dependence by fixing the order.
- Your suite is green as a whole but a single scenario fails when run alone. What does that tell you?That the scenario depends on state another scenario left behind — a record, a session, a cached value or a queue entry. Running one scenario in isolation is the cheapest detector for this, which is why it is worth running a random single scenario in the pipeline. The fix is to make the context steps establish everything the scenario needs, not to pin the order.
- When is a shared context object the wrong answer, and what replaces it?When steps are passing objects around because they are really one procedure split across three sentences. Then the fix is in the definitions: push the work into a helper the steps call, and let the context carry only identifiers. A context object with dozens of fields is a symptom of that, not a design.
- How do you decide between cleaning data up and generating unique data per scenario?Prefer unique data: it costs nothing on the failure path, survives parallel execution, and cannot half-run. Clean up when the data is expensive, when the environment is shared and would grow without bound, or when an external system enforces uniqueness you cannot salt around — and then do it in a hook, idempotently.
The context object is the scratch pad handed to one patient's care team and shredded when they leave; a process-wide variable is a whiteboard in the corridor that the next patient's team reads by mistake.
saying these in an interview costs you the question
- Stores scenario data in a process-wide or module-level variable
- Fixes order dependence by pinning the execution order
- Puts cleanup in a final Then step of the scenario
- Treats state-leak failures as ordinary flakiness
- Lets the context object grow into an untyped bag of keys
- Assumes data created outside the process cleans itself up