1. Introduction
Cucumber is a testing tool that supports Behavior-Driven Development (BDD) methodology. BDD empowers non-technical stakeholders to describe business features using natural domain-specific language.
The building blocks for defining Cucumber tests are features, scenarios, and steps, which must written in the Gherkin language. In this tutorial, we’ll learn how to share data between steps in Cucumber.
2. Setup
We’ll use a simple Spring Boot application that handles events to showcase how to share data between Cucumber steps. We’ll write BDD tests to verify the lifecycle of events, starting from their initial state when they enter our system to their final state after processing.
First, let’s define our Event class:
public class Event {
private String uuid;
private EventStatus status;
// standard getters and setters
}
public enum EventStatus {
PROCESSING, ERROR, COMPLETE
}
When we process an event, it transitions from the initial PROCESSING state to a final state, either COMPLETE or ERROR.
Now, let’s write our initial scenario and the step definitions:
Scenario: new event is properly initialized
When new event enters the system
Then event is properly initialized
In the next section, we’ll see how to share the event data between the two steps.
3. Share Data Between Steps Using Spring
The @ScenarioScope annotation allows us to share the state between different steps in our scenarios. The annotation instructs Spring to create a new instance for each scenario, enabling step definitions to share data between them and making sure the state is not leaked between different scenarios.
Before implementing the step definitions for the initial scenario, let’s also implement the SharedEvent test class, which will store the state to be shared between the steps:
@Component
@ScenarioScope
public class SharedEvent {
private Event event;
private Instant createdAt;
private Instant processedAt;
// standard getters and setters
}
The SharedEvent class compliments the Event class by introducing the additional fields createdAt and processedAt, helping us to define more elaborate scenarios.
Finally, let’s define the steps used in the scenario:
public class EventSteps {
static final String UUID = "1ed80153-666c-4904-8e03-08c4a41e716a";
static final String CREATED_AT = "2024-12-03T09:00:00Z";
@Autowired
private SharedEvent sharedEvent;
@When("new event enters the system")
public void createNewEvent() {
Event event = new Event();
event.setStatus(EventStatus.PROCESSING);
event.setUuid(UUID);
sharedEvent.setEvent(event);
sharedEvent.setCreatedAt(Instant.parse(CREATED_AT));
}
@Then("event is properly initialized")
public void verifyEventIsInitialized() {
Event event = sharedEvent.getEvent();
assertThat(event.getStatus()).isEqualTo(EventStatus.PROCESSING);
assertThat(event.getUuid()).isEqualTo(UUID);
assertThat(sharedEvent.getCreatedAt().toString()).isEqualTo(CREATED_AT);
assertThat(sharedEvent.getProcessedAt()).isNull();
}
}
Running the feature file, we see that the createNewEvent() and verifyEventIsInitialized() methods successfully share data through the SharedEvent class.
3.1. Extended Scenarios
Now, let’s take things further and write two other scenarios, each with multiple steps:
Scenario: event is processed successfully
Given new event enters the system
When event processing succeeds
Then event has COMPLETE status
And event has processedAt
Scenario: event is is not processed due to system error
Given new event enters the system
When event processing fails
Then event has ERROR status
And event has processedAt
Let’s also add the new step definitions to the EventSteps class. This showcases additional Gherkin features that improve step readability and reusability.
First, let’s implement the When step:
static final String PROCESSED_AT = "2024-12-03T10:00:00Z";
@When("event processing (succeeds|fails)$")
public void processEvent(String processingStatus) {
// process event ...
EventStatus eventStatus = "succeeds".equalsIgnoreCase(processingStatus) ?
EventStatus.COMPLETE : EventStatus.ERROR;
sharedEvent.getEvent().setStatus(eventStatus);
sharedEvent.setProcessedAt(Instant.parse(PROCESSED_AT));
}
The string “event processing (succeeds|fails)$” is a regular expression that allows reusing this step definition, matching both “event processing succeeds” and “event processing fails” steps. The $ character at the end of the regular expression ensures that the step matches exactly, without including any trailing characters.
Next, let’s implement the Then step responsible for checking the processedAt field:
@Then("event has processedAt")
public void verifyProcessedAt() {
assertThat(sharedEvent.getProcessedAt().toString()).isEqualTo(PROCESSED_AT);
}
Finally, let’s add the last Then step, which verifies the event status:
@Then("event has {status} status")
public void verifyEventStatus(EventStatus status) {
assertThat(sharedEvent.getEvent().getStatus()).isEqualTo(status);
}
@ParameterType("PROCESSING|ERROR|COMPLETE")
public EventStatus status(String statusName) {
return EventStatus.valueOf(statusName);
}
Parameter types in Cucumber allow us to convert method parameters from Cucumber expressions to objects. The @ParameterType status() method converts a String to an EventStatus enum value. This is invoked using the {status} placeholder in the step definition*.* Therefore, our step definition will match three steps:
- event has PROCESSING status
- event has COMPLETE status
- event has ERROR status
The new scenarios are now ready, and when running them, we notice that the multiple steps successfully share data.
4. Conclusion
In this tutorial, we’ve learned how to share data between steps in Cucumber. This feature allows us to share data between steps in a scenario while keeping the data isolated across multiple scenarios. It’s worth mentioning that sharing data between steps tightly couples them, thus reducing their reusability.
As always, the complete code can be found over on GitHub.