Scenario Context Handling in Cucumber JVM

What Is Scenario Context?

Scenario Context in Cucumber JVM is a mechanism used to share data between different step definition methods within the same scenario. A Cucumber scenario is made of multiple steps, and each step is implemented by a separate Java method. A local variable created inside one step definition is not automatically available inside another step definition. Scenario Context solves this problem by acting as temporary storage for data that must be created in one step and reused in a later step.

In simple terms, Scenario Context stores data created in one step so later steps in the same scenario can use it. If a login step creates a user session, a search step may need that session. If an API step creates a customer and receives a customer ID, a later update step may need the same ID. If a checkout step creates an order, the verification step may need the order number. Scenario Context gives the framework a clean place to hold those values for the lifetime of the scenario.

Scenario Context is not meant to be a global storage area for the entire test suite. It should be scoped to one scenario. When Scenario A starts, it gets its own context. When Scenario A ends, that context should be discarded. When Scenario B starts, it gets a fresh context. This isolation is important because scenarios should not depend on each other's data. It also makes parallel execution safer because each scenario has its own state.

Good Scenario Context handling makes step definitions cleaner and more realistic. Real workflows often produce values that later actions need. A user logs in, receives a token, searches for a product, adds it to a cart, creates an order, and verifies confirmation. Those values should not be recreated in every step. They should move through the scenario in a controlled way. Scenario Context provides that controlled flow.

Why Scenario Context Is Needed

Scenario Context is needed because step definitions are separate Java methods. A variable declared inside one method has local scope. It disappears when that method finishes. This is normal Java behavior, but it creates a practical problem in Cucumber because scenario steps often depend on information from previous steps. Without a shared context, later steps cannot easily access data produced earlier.

Scenario: Login and Search
  Given the user logs in
  When the user searches for "Laptop"
  Then the search results should contain "Laptop"

In this scenario, the login step may create a logged-in user or session. The search step may need the logged-in state. The verification step may need the search results. If the login step stores everything only in local variables, the search and verification steps cannot access them. The scenario reads like one connected flow, but the Java implementation is split across methods.

Scenario Context becomes the connection point. The login step stores the user or session. The search step retrieves what it needs and stores the results. The verification step retrieves the results and performs assertions. Each step remains focused, but the scenario data flows through the context.

The Problem Without Scenario Context

Without Scenario Context, teams often reach for poor alternatives. They may duplicate work in every step, use static variables, pass data through awkward step text, or combine too much behavior into one step definition. Each workaround creates maintenance problems. Static variables are especially dangerous because they can leak data between scenarios and break parallel execution.

@Given("the user logs in")
public void login() {
    User user = loginService.login();
}

@When("the user views the profile")
public void profile() {
    // user variable is not available here
}

The user variable exists only inside the login method. Once the method finishes, the profile step cannot use it. A developer may be tempted to make the variable static or place it in a global holder, but that creates data leakage. If another scenario runs afterward or in parallel, the value may be overwritten or reused incorrectly.

Scenario Context gives the framework a safer model. Store the user in scenario-scoped context during login. Retrieve it during profile verification. The state is shared only where it should be shared: inside the same scenario.

How Scenario Context Works

The context flow is straightforward. A step creates or receives data. It stores that data in Scenario Context. A later step retrieves the data from the same context. If the later step produces another value, it can store that value too. The context remains available until the scenario ends.

Given Step
   |
   v
Store User
   |
   v
Scenario Context
   |
   v
When Step
   |
   v
Retrieve User
   |
   v
Store Search Result
   |
   v
Then Step
   |
   v
Retrieve Search Result

This is not complicated, but it must be implemented carefully. The context should be easy to use, scenario-scoped, and safe for parallel execution. It should not become a dumping ground for every object created during automation. Store only values that later steps need. If a value is used only inside one step, keep it local.

What Kind of Data Should Be Stored?

Scenario Context commonly stores logged-in users, session IDs, authentication tokens, order IDs, customer IDs, API responses, search results, selected products, payment details, generated email addresses, uploaded file names, temporary test data, and business objects created during the scenario. These are values that connect one step to another.

For example, an API test may create a customer and receive a customer ID. The update step needs that ID. The delete step needs the same ID. The cleanup hook may also need it. Storing the ID in context is reasonable. A UI test may search for a product and store the selected product object so the checkout verification can compare the confirmation details. That is also a good use.

Do not store everything. If a value is not needed later, it does not belong in Scenario Context. Overloaded context becomes hard to understand and can hide poor step design. The goal is controlled data sharing, not unlimited memory for the scenario.

Scenario Scope and Test Independence

Scenario Context should exist only for one scenario. Scenario A should not share context data with Scenario B. This ensures test independence. Each scenario starts with a clean context, uses it during execution, and discards it at the end. This matches the way Cucumber scenarios should be designed: independent, repeatable, and safe to run in any order.

Scenario A
  |
  v
Create Context
  |
  v
Execute
  |
  v
Destroy Context

Scenario B
  |
  v
New Context
  |
  v
Execute
  |
  v
Destroy Context

This isolation is essential for parallel execution. If scenarios share state, one scenario can overwrite another scenario's data. Scenario Context should be implemented so each scenario has its own instance or scenario-scoped storage. Dependency injection frameworks such as PicoContainer, Spring, or Guice can help with this by injecting the same context instance into all step classes for one scenario while creating a new instance for the next scenario.

Basic Scenario Context Example

Consider a user profile scenario. The user logs in, opens the profile page, and verifies that the profile displays the username. The login step creates or retrieves the user. The profile step needs that user. The verification step needs the same user. Scenario Context is the natural place to store it.

Scenario: User Login
  Given the user logs in
  When the user views the profile
  Then the profile should display the username

The execution flow is: login, store user, retrieve user, open profile, retrieve user, verify username. The scenario reads as one flow, and the context carries the data through the flow.

Simple Context Class with Dedicated Fields

The simplest Scenario Context class uses dedicated fields. If the scenario only needs a user, the class can have a User field with getter and setter methods. This approach is easy to understand and type-safe because the getter returns a User, not a generic object.

public class ScenarioContext {

    private User user;

    public void setUser(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }
}

Dedicated fields are good when the number of shared objects is small and stable. They are readable because the context clearly says what it stores. A new team member can open the class and see that it stores a user, order, product, or token. The downside is that the class may grow as more fields are added. If many unrelated values are added, the context can become bulky.

Using Scenario Context in Step Definitions

Once the context class exists, step definitions can use it to store and retrieve data. In a dependency injection setup, the same Scenario Context instance is injected into each step definition class for the current scenario. In smaller examples, it may be created and shared by framework code, but dependency injection is cleaner for larger projects.

@Given("the user logs in")
public void login() {
    User user = loginService.login();
    scenarioContext.setUser(user);
}

@When("the user views the profile")
public void profile() {
    User user = scenarioContext.getUser();
    profilePage.open(user);
}

@Then("the profile should display the username")
public void verify() {
    User user = scenarioContext.getUser();
    profilePage.verify(user);
}

The step definitions remain clear. The login step stores the user. The profile and verification steps retrieve the user. There is no static variable and no duplicated login call. The data flow matches the scenario flow.

Storing Multiple Objects

Real scenarios may need more than one shared object. A checkout scenario may need a user, product, cart, order, payment token, and confirmation number. A context class can include several fields for these values.

public class ScenarioContext {

    private User user;
    private Order order;
    private Product product;
    private String token;
}

This is still readable if the objects are related and commonly used. However, do not add fields carelessly. Scenario Context should remain lightweight. If the context contains dozens of fields from unrelated modules, it may indicate that the framework needs more focused context objects or better step design.

Generic Context Using a Map

Another common implementation uses a map. Instead of dedicated fields, the context stores key-value pairs. This is flexible because any value can be stored under a key. It is useful when the framework has many different data types and the team wants one generic context mechanism.

public class ScenarioContext {

    private Map<String, Object> context =
            new HashMap<>();

    public void set(String key, Object value) {
        context.put(key, value);
    }

    public Object get(String key) {
        return context.get(key);
    }
}

Usage looks like this:

scenarioContext.set("USER", user);
scenarioContext.set("TOKEN", token);

User user = (User) scenarioContext.get("USER");

The map approach is flexible but weaker in type safety. The key is a string, so spelling mistakes become runtime errors. The value is an object, so callers must cast it to the expected type. This is manageable, but it should be improved with conventions or enum keys.

Using Enum Keys

Enum keys are recommended when using a generic map-based context. Instead of string keys such as "USER" or "TOKEN", define an enum. This reduces spelling mistakes and makes refactoring easier.

public enum ContextKey {
    USER,
    ORDER,
    PRODUCT,
    TOKEN
}

The context can then use ContextKey instead of strings.

private Map<ContextKey, Object> context =
        new HashMap<>();

public void set(ContextKey key, Object value) {
    context.put(key, value);
}

public Object get(ContextKey key) {
    return context.get(key);
}

Usage becomes safer:

scenarioContext.set(ContextKey.USER, user);

User user = (User) scenarioContext.get(ContextKey.USER);

Enum keys are not fully type-safe because values are still objects, but they eliminate key spelling mistakes. For many Cucumber frameworks, this is a practical balance between flexibility and maintainability.

Scenario Context in Selenium Automation

In Selenium automation, Scenario Context often stores business objects and runtime values generated through UI workflows. A login step may store the logged-in user. A product search step may store the selected product. A checkout step may store the created order. The confirmation step can retrieve the order and verify details on the confirmation page.

Login
  |
  v
Store Logged-in User
  |
  v
Search Product
  |
  v
Store Product
  |
  v
Checkout
  |
  v
Store Order
  |
  v
Verify Confirmation

This keeps the Selenium steps connected without relying on static variables. Page objects can receive the needed object from the step definition. The context holds scenario data, while page objects handle UI interaction. This separation keeps the framework cleaner.

Scenario Context in API Testing

API testing often depends heavily on IDs and responses created during the scenario. A scenario may create a customer, store the customer ID, update the customer, retrieve the ID again, validate the update, and then delete the customer during cleanup. Scenario Context prevents repeated calls and makes the data flow clear.

Create Customer
  |
  v
Store Customer ID
  |
  v
Update Customer
  |
  v
Retrieve Customer ID
  |
  v
Delete Customer

API responses can also be stored when later steps need them. For example, a Response object from Rest Assured may be stored after a request and retrieved in assertion steps. However, avoid storing every response unnecessarily. Store only what later steps actually validate or use.

Scenario Context Versus Global Variables

Global variables and static fields are a poor way to share scenario data. A static field is shared across scenarios, and that creates data leakage. One scenario can overwrite another scenario's value. Parallel execution becomes unsafe. Tests may pass locally but fail randomly in CI.

public class SharedData {
    public static User user;
}

This may seem convenient, but it breaks scenario isolation. Scenario Context is safer because it is scoped to one scenario. Each scenario gets its own context values. When the scenario ends, the values are discarded. This is why Scenario Context is preferred over global variables in Cucumber frameworks.

Scenario Context Versus ThreadLocal

ThreadLocal is a lower-level Java mechanism that stores values per thread. It is often used for WebDriver in parallel Selenium execution. Scenario Context is a higher-level test design concept that stores business or scenario data. The two are related but not the same.

Scenario Context is business-oriented. It may store user, order, token, product, response, or generated IDs. ThreadLocal is thread-oriented. It may store driver instances or other thread-specific resources. In some enterprise frameworks, Scenario Context itself may be implemented using ThreadLocal internally to support parallel execution. Step definitions should still interact with a clean Scenario Context API rather than scattering ThreadLocal logic everywhere.

Scenario Context and Dependency Injection

Dependency injection frameworks such as PicoContainer, Spring, and Guice can inject a shared Scenario Context object into multiple step definition classes. This is one of the cleanest ways to share state across steps without static variables. The context instance is created for a scenario and injected wherever needed.

Scenario Context
      |
      v
Injected into
      |
      v
LoginSteps
SearchSteps
CheckoutSteps

With dependency injection, LoginSteps can store the user, SearchSteps can retrieve the user and store the product, and CheckoutSteps can retrieve both. The step classes remain separate, but they share the same scenario-scoped context. This is much cleaner than making all steps live in one huge class just to share fields.

Real-Time Order Placement Example

Consider an order placement scenario. The customer logs in, places an order, and verifies confirmation. The login step stores the customer. The order step retrieves the customer and creates an order. The confirmation step retrieves the order and verifies the confirmation message.

Scenario: Place Order
  Given the customer logs in
  When the customer places an order
  Then the order confirmation should be displayed
Login
  |
  v
Store Customer
  |
  v
Place Order
  |
  v
Store Order
  |
  v
Verify Confirmation
  |
  v
Retrieve Order

This is a strong use of Scenario Context because the values are genuinely part of the same scenario flow. The context supports the business journey without hiding behavior.

Enterprise Framework Structure

A larger Cucumber JVM framework often places context classes in a dedicated package. The package may include ScenarioContext and ContextKey. Step definitions, pages, services, and models remain separate. This structure makes context easy to find and keeps data-sharing logic centralized.

src
 ├── context
 │      ├── ScenarioContext.java
 │      └── ContextKey.java
 ├── stepdefinitions
 │      ├── LoginSteps.java
 │      ├── SearchSteps.java
 │      └── CheckoutSteps.java
 ├── pages
 ├── services
 └── models

A generic enum-key context may look like this:

public class ScenarioContext {

    private final Map<ContextKey, Object> context =
            new HashMap<>();

    public void set(ContextKey key, Object value) {
        context.put(key, value);
    }

    public Object get(ContextKey key) {
        return context.get(key);
    }
}

This design is flexible and organized. It keeps context operations in one class and avoids random static state across step definitions.

Typed Helper Methods for Safer Context Access

A generic map-based context is flexible, but repeated casting in step definitions can become noisy. A practical improvement is to keep the internal map generic while exposing typed helper methods for common objects. For example, the context can have setUser, getUser, setOrder, and getOrder methods. Internally, those methods may still use enum keys, but step definitions get a cleaner API.

public void setUser(User user) {
    set(ContextKey.USER, user);
}

public User getUser() {
    return (User) get(ContextKey.USER);
}

public void setOrder(Order order) {
    set(ContextKey.ORDER, order);
}

public Order getOrder() {
    return (Order) get(ContextKey.ORDER);
}

This approach improves readability. A step definition can call scenarioContext.getUser() instead of casting from a generic map every time. It also centralizes casting in one place. If the internal implementation changes later, step definitions do not need to change. This is a useful balance for enterprise frameworks that need flexibility but still want readable step code.

Typed helpers should be used for frequently shared objects. Rare or module-specific values can still use generic access if necessary. The context should remain practical, not over-engineered.

Handling Missing Context Values

A common runtime problem occurs when a step tries to retrieve a value that was never stored. For example, a profile verification step may call getUser(), but the login step did not run or failed before storing the user. If the context simply returns null, the next error may be a confusing NullPointerException in a page object. A better context design fails clearly when required data is missing.

public Object getRequired(ContextKey key) {
    if (!context.containsKey(key)) {
        throw new IllegalStateException(
            "Scenario context value is missing: " + key);
    }
    return context.get(key);
}

This makes failures easier to diagnose. Instead of seeing a vague null pointer later, the framework reports that a required context value is missing. The team can then check whether the scenario flow is correct, whether the earlier step stored the value, or whether the wrong context key was used.

Not every context value is required. Sometimes optional values are acceptable, especially in cleanup hooks where a resource may or may not have been created. For required business flow data, fail fast with a clear message. For optional cleanup data, check safely and skip when absent.

Scenario Context and Cleanup Hooks

Scenario Context is useful not only for steps but also for cleanup hooks. During a scenario, the framework may create users, orders, uploaded files, or API records. The identifiers for those resources can be stored in context. An @After hook can then retrieve the identifiers and clean only the data created by the current scenario.

scenarioContext.set(ContextKey.CUSTOMER_ID, customer.getId());
scenarioContext.set(ContextKey.ORDER_ID, order.getId());
@After
public void cleanup() {
    String orderId = scenarioContext.getOrderId();
    if (orderId != null) {
        orderService.deleteOrder(orderId);
    }
}

This is safer than broad cleanup. The hook does not need to delete all test orders or guess which records belong to the scenario. It uses the exact identifiers captured during execution. This pattern is especially important in parallel runs, where multiple scenarios may create data at the same time.

Cleanup-related context should be specific and traceable. Store resource IDs, file keys, generated emails, and run-specific identifiers. Avoid storing large unrelated objects just for cleanup. The cleanup hook needs enough information to delete resources safely, not the entire scenario history.

Parallel-Safe Scenario Context

Parallel execution is one of the strongest reasons to avoid static variables and use proper Scenario Context. When scenarios run in parallel, each scenario must have its own context instance. If all scenarios share one map or one static field, data will collide. Scenario A may store User A, then Scenario B may overwrite it with User B before Scenario A reaches verification. The result is random failure.

A parallel-safe design creates a separate context for each scenario. Dependency injection frameworks can provide scenario-scoped instances. A ThreadLocal-based framework can store context per thread, though care must be taken when thread reuse occurs. The important rule is that one scenario's context data must not be visible to another scenario.

After the scenario ends, context data should be cleared or discarded. If ThreadLocal is used internally, remove the value after execution to avoid memory leaks or stale data reuse when a thread is reused by another scenario. If dependency injection is used, rely on scenario scope to create and destroy instances correctly.

@After
public void clearContext() {
    scenarioContext.clear();
}

In many frameworks, clearing is handled automatically because the context instance is discarded. If the context is manually managed, explicit cleanup is important. Parallel-safe context design makes Cucumber suites more stable in CI pipelines.

Common Mistakes with Scenario Context

Using Static Variables

Static variables are the most common mistake. They are shared across scenarios and unsafe for parallel execution. Use scenario-scoped context instead.

Storing Everything

Scenario Context should store only data needed by later steps or cleanup. Do not store every local variable. A bloated context becomes hard to understand.

Using String Keys Everywhere

String keys can cause runtime errors when keys are misspelled. Use enum keys or typed methods to reduce mistakes.

Forgetting Scenario Scope

Context should be discarded after each scenario. Reusing context across scenarios creates hidden dependencies and data leakage.

Mixing Business Logic into Context

Scenario Context should store and retrieve data. It should not perform login, search, checkout, API calls, or assertions. Business logic belongs in services, page objects, or step definitions.

Best Practices for Scenario Context

Use Scenario Context only for data that must be shared between steps in the same scenario. Keep the context scenario-scoped to maintain test independence. Prefer strongly typed objects such as POJOs over raw strings or generic maps whenever possible. If using a generic map, prefer enum keys instead of string keys.

Avoid static variables for scenario data. Inject Scenario Context using dependency injection frameworks such as PicoContainer, Spring, or Guice when building larger frameworks. Remove unnecessary data from context or avoid storing it in the first place. Keep context lightweight and easy to inspect.

Design context for parallel execution. Each scenario should have its own data. Do not let one scenario's context leak into another. If using ThreadLocal internally, hide that implementation behind a clean context API so step definitions stay readable.

Interview-Ready Explanation

Scenario Context is a temporary storage mechanism used to share data between step definitions within the same Cucumber scenario. It exists only for the lifetime of one scenario, which ensures test isolation and supports parallel execution. Common data stored in Scenario Context includes users, tokens, IDs, API responses, search results, orders, products, and other business objects required by later steps.

Scenario Context can be implemented with dedicated fields or a generic map. If using a map, enum keys are preferred over string keys to reduce spelling mistakes. In enterprise frameworks, Scenario Context is often injected into step definition classes using dependency injection tools such as PicoContainer, Spring, or Guice. It should store only scenario data and should never replace business logic or rely on static variables.

Summary

Scenario Context handling is essential for clean Cucumber JVM automation because real scenarios often need data to move from one step to another. A value created in a Given step may be needed in a When step and verified in a Then step. Scenario Context provides a controlled, scenario-scoped way to share that data without static variables or duplicated work.

The golden rules are clear: Scenario Context is shared only within one scenario, store only data needed by later steps, never use static variables for scenario data, prefer POJOs and enum keys over raw strings and generic objects, and keep Scenario Context focused on data sharing rather than business logic. When implemented well, Scenario Context makes Cucumber frameworks cleaner, safer, and more maintainable.