Data-Driven Testing in Cucumber
Data Tables, Scenario Outline, transformers
Data Tables vs Scenario Outline
Cucumber has two ways to work with data. Understanding the difference is critical:
| Aspect | Data Table | Scenario Outline |
|---|---|---|
| Purpose | Pass multiple values to a single step | Run the entire scenario with different data |
| Execution | Scenario runs once | Scenario runs N times (once per row) |
| Placement | Under a single step | Under the scenario with Examples: |
| Use Case | Fill a form, create multiple records | Test login with 5 different credential sets |
# DATA TABLE — runs once, table passed to one step
Scenario: Fill registration form
When I fill in the form:
| firstName | John |
| lastName | Doe |
| email | john@test.com |
Then registration should succeed
# SCENARIO OUTLINE — runs 3 times (once per Examples row)
Scenario Outline: Login attempts
When I login with "<user>" and "<pass>"
Then I should see "<result>"
Examples:
| user | pass | result |
| admin | admin1 | Dashboard |
| user | wrong | Error |
| guest | | Error |
Data Tables — All Formats
Format 1: Key-Value Pairs (Map)
Scenario: Update user profile
When I update my profile with:
| firstName | John |
| lastName | Doe |
| email | john@example.com |
| phone | 1234567890 |
@When("I update my profile with:")
public void iUpdateProfileWith(DataTable dataTable) {
// Convert to Map (2-column table without header)
Map<String, String> data = dataTable.asMap(String.class, String.class);
profilePage.enterFirstName(data.get("firstName")); // "John"
profilePage.enterLastName(data.get("lastName")); // "Doe"
profilePage.enterEmail(data.get("email")); // "john@example.com"
profilePage.enterPhone(data.get("phone")); // "1234567890"
}
Format 2: Single Column (List)
Scenario: Verify available categories
Then I should see the following categories:
| Electronics |
| Clothing |
| Books |
| Sports |
@Then("I should see the following categories:")
public void iShouldSeeCategories(DataTable dataTable) {
// Convert to flat list
List<String> expectedCategories = dataTable.asList();
// ["Electronics", "Clothing", "Books", "Sports"]
List<String> actualCategories = categoryPage.getCategories();
Assert.assertEquals(expectedCategories, actualCategories);
}
Format 3: Multiple Rows with Headers (List of Maps)
Scenario: Create multiple users
When I create the following users:
| username | email | role |
| john | john@test.com | admin |
| jane | jane@test.com | user |
| bob | bob@test.com | viewer |
@When("I create the following users:")
public void iCreateUsers(DataTable dataTable) {
// First row is the header, remaining rows are data
List<Map<String, String>> users = dataTable.asMaps();
for (Map<String, String> user : users) {
userService.createUser(
user.get("username"), // "john", "jane", "bob"
user.get("email"), // "john@test.com", ...
user.get("role") // "admin", "user", "viewer"
);
}
}
// users.size() == 3 (header row is not counted)
Format 4: Lists of Lists
@When("I verify the price list:")
public void verifyPriceList(DataTable dataTable) {
// Raw access — each row is a List<String>
List<List<String>> rows = dataTable.asLists();
// rows.get(0) = ["Product", "Price"] (header)
// rows.get(1) = ["Laptop", "999.99"]
// rows.get(2) = ["Mouse", "29.99"]
for (int i = 1; i < rows.size(); i++) { // skip header
String product = rows.get(i).get(0);
String price = rows.get(i).get(1);
Assert.assertEquals(price, productPage.getPrice(product));
}
}
Quick Reference — DataTable Methods
| Method | Returns | Best For |
|---|---|---|
asMap() | Map<String, String> | 2-column key-value pairs |
asMaps() | List<Map<String, String>> | Multiple rows with headers |
asList() | List<String> | Single column flat list |
asLists() | List<List<String>> | Raw access to all cells |
transpose() | DataTable | Flip rows and columns |
cell(row, col) | String | Single cell access |
@DataTableType — Transform to Objects
Instead of working with raw Maps, transform data table rows directly into Java objects using @DataTableType:
The POJO
public class User {
private String username;
private String email;
private String role;
public User(String username, String email, String role) {
this.username = username;
this.email = email;
this.role = role;
}
// Getters
public String getUsername() { return username; }
public String getEmail() { return email; }
public String getRole() { return role; }
}
The Transformer
import io.cucumber.java.DataTableType;
public class DataTableTransformers {
@DataTableType
public User userEntry(Map<String, String> entry) {
return new User(
entry.get("username"),
entry.get("email"),
entry.get("role")
);
}
}
The Step Definition — Clean!
@When("I create the following users:")
public void iCreateUsers(List<User> users) {
// Cucumber automatically converts each row to a User object
for (User user : users) {
userService.createUser(user);
System.out.println("Created: " + user.getUsername());
}
}
// Or even simpler with Java streams
@When("I create the following users:")
public void iCreateUsers(List<User> users) {
users.forEach(userService::createUser);
}
More @DataTableType Examples
// Product with type conversion
@DataTableType
public Product productEntry(Map<String, String> entry) {
return new Product(
entry.get("name"),
Double.parseDouble(entry.get("price")),
Integer.parseInt(entry.get("quantity"))
);
}
// Single-cell table entry (for single-column tables)
@DataTableType
public Category categoryEntry(String cell) {
return new Category(cell);
}
Scenario Outline — Full Guide
Scenario Outline is a template that runs the entire scenario once for each row in the Examples table. Placeholders in angle brackets (<name>) get replaced with values from each row.
Basic Example
Scenario Outline: Calculator operations
Given I have a calculator
When I perform "<operation>" on <a> and <b>
Then the result should be <result>
Examples:
| operation | a | b | result |
| add | 5 | 3 | 8 |
| subtract | 10 | 4 | 6 |
| multiply | 3 | 7 | 21 |
| divide | 20 | 5 | 4 |
This generates 4 separate scenarios — one for each row.
@When("I perform {string} on {int} and {int}")
public void iPerform(String operation, int a, int b) {
switch (operation) {
case "add" -> result = a + b;
case "subtract" -> result = a - b;
case "multiply" -> result = a * b;
case "divide" -> result = a / b;
}
}
@Then("the result should be {int}")
public void resultShouldBe(int expected) {
Assert.assertEquals(result, expected);
}
Multiple Examples Tables
You can have multiple Examples blocks, each with its own name and tags:
Scenario Outline: User role permissions
Given I am logged in as "<role>"
When I navigate to "<page>"
Then access should be "<access>"
@admin-tests
Examples: Admin permissions
| role | page | access |
| admin | dashboard | granted |
| admin | settings | granted |
| admin | user-mgmt | granted |
| admin | billing | granted |
@user-tests
Examples: Regular user permissions
| role | page | access |
| user | dashboard | granted |
| user | settings | denied |
| user | user-mgmt | denied |
| user | billing | denied |
@guest-tests
Examples: Guest permissions
| role | page | access |
| guest | dashboard | denied |
| guest | settings | denied |
| guest | user-mgmt | denied |
| guest | billing | denied |
Combining Outline with Data Tables
You can use placeholders inside data tables within a Scenario Outline:
Scenario Outline: Register user by type
When I register a new "<type>" user:
| firstName | <firstName> |
| lastName | <lastName> |
| email | <email> |
Then the user should be created with role "<expectedRole>"
Examples:
| type | firstName | lastName | email | expectedRole |
| customer | John | Doe | john@test.com | CUSTOMER |
| vendor | Jane | Smith | jane@vendor.com | VENDOR |
| admin | Bob | Admin | bob@company.com | ADMIN |
The placeholders in the data table get replaced just like those in regular steps, so each Examples row generates a complete scenario with its own data table values.
Real-World Patterns
Pattern 1: Login Test Matrix
@regression
Scenario Outline: Login validation matrix
Given I am on the login page
When I enter username "<username>" and password "<password>"
And I click login
Then I should see "<message>"
And the response code should be <code>
@positive
Examples: Valid credentials
| username | password | message | code |
| admin | Admin@123 | Welcome | 200 |
| testuser | Test@456 | Welcome | 200 |
@negative
Examples: Invalid credentials
| username | password | message | code |
| admin | wrongpass | Invalid credentials | 401 |
| unknown | Admin@123 | User not found | 404 |
| | Admin@123 | Username required | 400 |
| admin | | Password required | 400 |
@security
Examples: Security scenarios
| username | password | message | code |
| admin' OR '1'='1 | anything | Invalid input | 400 |
| admin | <script> | Invalid input | 400 |
Pattern 2: Form Validation with Data Tables
Scenario: Validate registration form fields
When I submit the registration form with:
| field | value | valid |
| email | john@test.com | true |
| email | invalid-email | false |
| email | | false |
| phone | 1234567890 | true |
| phone | 123 | false |
| phone | abc | false |
| password | Str0ng@Pass | true |
| password | weak | false |
| password | 12345678 | false |
@When("I submit the registration form with:")
public void validateFields(DataTable dataTable) {
List<Map<String, String>> rows = dataTable.asMaps();
for (Map<String, String> row : rows) {
String field = row.get("field");
String value = row.get("value");
boolean expectedValid = Boolean.parseBoolean(row.get("valid"));
boolean actualValid = validationHelper.isValid(field, value);
Assert.assertEquals(
actualValid, expectedValid,
field + " with value '" + value + "' expected "
+ (expectedValid ? "valid" : "invalid")
);
}
}
Pattern 3: API Response Verification
Scenario: Verify user API response fields
Given I request user details for "john_doe"
Then the response should contain:
| field | value |
| username | john_doe |
| email | john@example.com |
| role | admin |
| isActive | true |
And the response should not contain:
| password |
| ssn |
| token |
Advanced Table Operations
Transpose
Flip rows and columns when the natural layout is horizontal:
# Horizontal layout (easier to read for single records)
Scenario: Verify product details
Then the product should have:
| name | price | category | inStock |
| Mouse | 29.99 | Electronics | true |
@Then("the product should have:")
public void verifyProduct(DataTable dataTable) {
// Transpose converts the single row into a map
Map<String, String> product = dataTable.transpose()
.asMap(String.class, String.class);
Assert.assertEquals("Mouse", product.get("name"));
Assert.assertEquals("29.99", product.get("price"));
Assert.assertEquals("Electronics", product.get("category"));
Assert.assertEquals("true", product.get("inStock"));
}
Cell Access
@Then("I verify the data:")
public void verifyData(DataTable dataTable) {
// Direct cell access (0-indexed)
String topLeft = dataTable.cell(0, 0);
String secondRowThirdCol = dataTable.cell(1, 2);
// Row count
int rows = dataTable.height();
// Column count
int cols = dataTable.width();
}
Diff Tables — Compare Expected vs Actual
@Then("the search results should be:")
public void verifySearchResults(DataTable expectedTable) {
// Build actual data table from the page
List<List<String>> actualData = new ArrayList<>();
actualData.add(List.of("name", "price", "rating")); // header
for (Product p : searchPage.getResults()) {
actualData.add(List.of(
p.getName(),
String.valueOf(p.getPrice()),
String.valueOf(p.getRating())
));
}
DataTable actualTable = DataTable.create(actualData);
// diff() throws an error with a clear diff if tables don't match
expectedTable.diff(actualTable);
}