Resource library

QA How-To

Sharing state between Cucumber steps (2026)

Learn safe patterns for sharing state between Cucumber steps using scenario context, dependency injection, World objects, cleanup, and parallel testing.

18 min read | 3,648 words

TL;DR

Use a scenario-scoped context or injected domain object, store identifiers rather than entire mutable responses when possible, and let one component own each resource. Never use static mutable fields for scenario data.

Key Takeaways

  • Share the minimum data needed to express the scenario flow.
  • Use scenario scope and constructor injection across step classes.
  • Store stable identifiers instead of large mutable response graphs.
  • Give each resource one owner and one cleanup path.
  • Prove isolation with shuffled and parallel execution.
  • Never call one step definition method from another.

sharing state between Cucumber steps is the practical discipline of passing only necessary scenario data through an isolated owner instead of globals or step-to-step calls. The reliable approach is to make lifecycle, ownership, and observable outcomes explicit, then keep framework glue smaller than the behavior it supports.

This guide moves from mental model to runnable implementation. It explains tradeoffs that appear in production suites, shows how to debug failures, and gives review criteria you can use with a team. Examples favor stable public APIs and avoid version-sensitive shortcuts.

TL;DR

Decision Recommended default Reason
Local variable One step only Best default
Injected scenario context Several step classes Recommended shared state
World object Cucumber-JS scenario Idiomatic JavaScript
Static field All scenarios and threads Avoid

Use a scenario-scoped context or injected domain object, store identifiers rather than entire mutable responses when possible, and let one component own each resource. Never use static mutable fields for scenario data.

1. Sharing State Between Cucumber Steps: Scope First

State belongs to the scenario that created it. Define its lifetime, owner, producer, consumers, and cleanup before choosing a container. Most failures called Cucumber problems are actually unclear state ownership. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Keep data local when only one step uses it. Promote a value to context only when a later step or another glue class needs it. A smaller shared surface reduces accidental coupling. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

2. Why Static Fields Fail

A static field belongs to the JVM class loader, not a scenario. Parallel workers overwrite values and sequential scenarios can consume leftovers. Reset hooks reduce symptoms but do not establish correct ownership. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

ThreadLocal is also a poor default for business state. Execution can involve framework-managed threads, and manual removal is easy to miss. Use the object factory lifecycle designed for scenarios. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

3. Runnable Java Scenario Context Pattern

With the Cucumber PicoContainer module present, constructor parameters are resolved in the same scenario-scoped object graph.

package example.steps;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import static org.junit.jupiter.api.Assertions.assertEquals;

final class OrderContext {
    String orderId;
}

final class OrderSteps {
    private final OrderContext context;
    private final OrderApi api;

    OrderSteps(OrderContext context, OrderApi api) {
        this.context = context;
        this.api = api;
    }

    @Given("a paid order")
    void createPaidOrder() {
        context.orderId = api.createPaidOrder();
    }

    @Then("the order status is {word}")
    void orderStatusIs(String expected) {
        if (context.orderId == null) {
            throw new IllegalStateException("A paid order must be created first");
        }
        assertEquals(expected, api.find(context.orderId).status());
    }
}

OrderApi is project code, while the annotations and Cucumber Expression placeholder are public Cucumber APIs. In production, inject a configured client rather than constructing one inside the step. See API test data management for external record isolation.

4. Scenario Context as a Data Contract

Design a context around meaningful scenario facts. Expose an order ID, last command result, or authenticated actor instead of a miscellaneous map. Typed fields make producers and consumers reviewable. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Separate domain state from infrastructure resources when lifecycles differ. An API session owner can close itself while the scenario facts remain plain data. One giant context eventually becomes a service locator. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

5. Dependency Injection Options

Cucumber supports object factories and integrations such as PicoContainer and Spring. Use constructor injection so dependencies are explicit and replaceable in focused tests. The container must create a fresh scenario graph. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Do not mix several DI strategies casually. Document scope annotations and verify them under parallel execution. Application singleton scope is not automatically safe for mutable test state. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

6. Store Values, IDs, or Objects

Prefer immutable values and stable identifiers when later steps can retrieve fresh state. An order ID avoids sharing a stale response object after the system changes. Re-querying also proves the observable outcome. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Share a rich object only when it represents the scenario aggregate and ownership is clear. Avoid exposing WebDriver, database connections, and arbitrary payloads through public fields everywhere. Narrow methods preserve invariants. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

7. State Across Given, When, and Then

Given establishes explicit preconditions and may record resulting identifiers. When performs the subject action and records only the response facts needed for verification. Then queries or compares observable state. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Do not make steps depend on undocumented order beyond the scenario narrative. Validate missing prerequisites with a clear context error. A null pointer hides the real contract violation. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

8. Cleanup and Failure Safety

Resource owners should close what they create. Use an After hook to invoke idempotent cleanup for scenario-scoped resources. Context cleanup should tolerate a failed Given or When. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Do not clear diagnostic data before artifacts are captured. Order capture and closure intentionally while keeping the primary failure. The hooks and tags guide covers conditional cleanup. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

9. Parallel Execution Proof

Run a small suite with unique scenario data and multiple workers. Add scenario identity to diagnostic logs and assert that contexts are distinct where practical. A pass in ordered single-thread mode proves little about isolation. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

External systems also need isolated records, accounts, and queues. A scenario-scoped Java object cannot prevent two scenarios from updating the same customer. Allocate unique data or lease shared fixtures safely. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

10. Cross-Scenario Workflows

Scenarios should remain independently executable. Do not pass an ID from one scenario to the next through a static cache or ordered runner. Retries and filters make such workflows nondeterministic. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Model a long business journey within one scenario when it is one behavior, or establish state through APIs for each scenario. Use lower-level workflow tests for exhaustive sequences. Independence keeps reports honest. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

11. sharing state between Cucumber steps Review Checklist

Inventory every write and read before replacing a global context. Group fields by owner, introduce typed scenario objects, then migrate one capability at a time. Characterization scenarios protect behavior during the change. This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Delete reset hooks after the global disappears and enable parallel tests gradually. Use the Cucumber Java step definition guide to keep consumers thin. Track context size in review rather than creating another . This matters in a real suite because a scenario should explain one business outcome while the automation supplies only the technical detail needed to prove it. When the implementation is explicit, a failed build tells the team what changed instead of forcing an engineer to reverse engineer hidden framework behavior.

A useful review question is: "Could another engineer predict the scope, timing, and failure mode from this code?" If the answer is no, simplify the boundary and give the shared object or helper a name that describes its responsibility. Keep assertions close to the observable outcome, log identifiers that help reproduce a failure, and remove cleanup logic that can mask the original exception. These habits make sharing state between Cucumber steps maintainable under parallel execution and routine product change.

Interview Questions and Answers

Q: undefined

undefined

Q: undefined

undefined

Q: undefined

undefined

Q: undefined

undefined

Q: undefined

undefined

Q: undefined

undefined

Q: undefined

undefined

Common Mistakes

  1. Using static fields: State leaks across scenarios and workers.

  2. Building a universal map: String keys remove type safety and ownership.

  3. Calling step methods: Glue becomes coupled to presentation instead of shared services.

  4. Sharing full mutable responses: Consumers observe stale or accidentally modified data.

  5. Resetting globals in hooks: A reset is not scenario scope and can race.

  6. Ignoring external collisions: Isolated objects still fail when scenarios share accounts or records.

Conclusion

Safe sharing state between Cucumber steps comes from explicit scenario scope, typed contracts, narrow ownership, and independent external test data. Start with one representative feature, run it alone and in parallel, and review the resulting report with both an automation engineer and a product stakeholder. That small exercise exposes unclear ownership early and gives the team a repeatable standard for the rest of the suite.

Related Guides