Using Cucumber Data Tables to Reduce Duplicate Scenarios in BDD Tests
Learn how Cucumber Data Tables let a single scenario drive many input rows, reducing duplication while adding considerations for traceability and type support.
01 Jan 2026, 16:48 UTC

Problem: repetitive scenarios bloat feature files
When a feature needs to be validated with many input combinations, teams often write a separate Scenario for each set of values. This leads to long feature files, duplicated step definitions, and extra effort when the underlying logic changes.
Thesis: Data Tables let a single scenario drive multiple rows of data
Cucumber's Data Table feature converts a pipe-separated block after a step into a collection that the step definition receives as an argument. By keeping the scenario generic and moving the variation into the table, you eliminate duplication while preserving readability.
Worked example: login validation with a Data Table
Suppose you need to verify that a login page accepts valid credentials and rejects invalid ones. Instead of writing five scenarios, you write one scenario with a table of username/password pairs.
Feature file (login.feature)
Feature: Login validation
Scenario: Validate login with a data table
Given the login page is displayed
When I attempt to log in with the following credentials:
| username | password |
| alice | secret123 |
| bob | wrong |
| charlie | !@#$ |
| dave | pass456 |
| eve | |
Then each login attempt should produce the expected outcome
The table sits directly after the When step. Cucumber parses each pipe-delimited row and passes the whole table to the step definition. (A Scenario Outline with an Examples block is an alternative for the same goal; the inline table shown here keeps everything in one scenario.)
Step definition (Java, Cucumber-Jupiter)
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class LoginSteps {
private LoginPage loginPage;
private List<String> outcomes;
@Given("the login page is displayed")
public void the_login_page_is_displayed() {
loginPage = new LoginPage(); // assumes a PageObject or similar
loginPage.open();
}
@When("I attempt to log in with the following credentials:")
public void i_attempt_to_log_in_with_the_following_credentials(
List<Map<String, String>> rows) {
outcomes = new ArrayList<>();
for (Map<String, String> row : rows) {
String user = row.get("username");
String pass = row.get("password");
outcomes.add(loginPage.submit(user, pass));
}
}
@Then("each login attempt should produce the expected outcome")
public void each_login_attempt_should_produce_the_expected_outcome() {
// assert outcomes against expected values here
}
}
The step definition receives a List<Map<String,String>> where each map corresponds to one row, keyed by the header names. The implementation iterates over the rows, performs the action, and stores outcomes for later verification.
Trade-off: traceability of failures
If an assertion fails inside the loop, the scenario stops and the report shows only the failing step, not which row caused it. To retain traceability:
- Add row-specific logging (for example, printing the username) before each action.
- Collect outcomes per row and assert after the loop, so the failure message can include the row's username or index.
- Alternatively, accept
io.cucumber.datatable.DataTabledirectly and callasMaps()to control how rows are converted.
Configuration and verification
To try this locally:
- Create a Maven project (or use an existing one).
- Add Cucumber dependencies (version 7.x is used here as a stable baseline):
<dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-java</artifactId> <version>7.15.0</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-junit</artifactId> <version>7.15.0</version> <scope>test</scope> </dependency> - Place the feature file under
src/test/resources/featuresand the step definition undersrc/test/java. - Run the tests with
mvn testfrom the project root. No special OS permissions are required; the command executes in the user's context. - After the run, check the console output or your HTML report (if you have a reporting plugin configured). Confirm that the scenario passes and that no "ambiguous step definition" warnings appear, which would indicate duplicate glue code.
Limitation: type support
Data Tables natively handle built-in types (String, int, boolean, and so on) and simple POJOs with a no-arg constructor and setters matching column names. For complex objects, such as nested structures or domain models, you must register a custom converter, which adds boilerplate and requires care when upgrading Cucumber versions.
Actionable closing
Adopt Data Tables whenever you find yourself writing multiple scenarios that differ only in input values. Keep the step definition focused on the business action, move validation outside the loop or add per-row assertions to preserve traceability, and periodically review the test report to confirm each table row is exercised and failures are easy to map back to specific inputs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.