A Scenario Outline runs the same scenario multiple times with different data. Sometimes you need a different pattern: pass a set of data into a single step — multiple users to create, multiple products to add to a cart, multiple permission entries to verify. That's what data tables are for.
What a data table looks like
Scenario: Create a team with multiple members
Given the following team members exist:
| name | email | role |
| Alice | alice@example.com | admin |
| Bob | bob@example.com | tester |
| Charlie | charlie@example.com| viewer |
When the admin views the team page
Then there should be 3 members listedThe table after the colon on the Given step is a data table — it's part of that step, passed as a DataTable parameter to the step definition. The scenario itself runs once; the table is input data to that single execution.
Reading a data table in Java
The step definition receives a DataTable from io.cucumber.datatable:
import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.Given;
import java.util.List;
import java.util.Map;
@Given("the following team members exist:")
public void theFollowingTeamMembersExist(DataTable dataTable) {
List<Map<String, String>> members = dataTable.asMaps();
for (Map<String, String> member : members) {
String name = member.get("name");
String email = member.get("email");
String role = member.get("role");
userService.createUser(name, email, role);
}
}dataTable.asMaps() converts the table into a List<Map<String, String>>. The header row becomes the map keys; each data row becomes one map. Iterating with a for loop processes one member per iteration.
DataTable conversion methods
The DataTable API has four conversions. Pick the one that matches your table shape:
asMaps() — named-column table:
List<Map<String, String>> rows = dataTable.asMaps();
// Each map: {"name": "Alice", "email": "alice@example.com", "role": "admin"}Use when the table has column headers and you want to access values by name. The most common case for test setup data.
asMap() — two-column key-value table:
Given the system configuration is:
| timeout | 30 |
| retries | 3 |
| logLevel | DEBUG |Map<String, String> config = dataTable.asMap();
// {"timeout": "30", "retries": "3", "logLevel": "DEBUG"}Use when you have a two-column lookup table with no header.
asLists() — raw rows including header:
List<List<String>> raw = dataTable.asLists();
// Row 0: ["name", "email", "role"] (header)
// Row 1: ["Alice", "alice@example.com", "admin"]Use when you need to process the header row yourself, or when the table doesn't have a fixed column structure.
asList() — single column of values:
Given the cart contains the following products:
| Laptop |
| Keyboard |
| Mouse |List<String> products = dataTable.asList();
// ["Laptop", "Keyboard", "Mouse"]Use for a simple list of values — no column name needed.
Typed conversion
asMaps() returns Map<String, String> — all values are strings. For typed data, use asMaps(String.class, Integer.class) or map to a POJO:
Given the following products are in stock:
| name | price | quantity |
| Laptop | 999 | 10 |
| Phone | 599 | 25 |@Given("the following products are in stock:")
public void productsInStock(DataTable dataTable) {
List<Map<String, String>> rows = dataTable.asMaps();
for (Map<String, String> row : rows) {
String name = row.get("name");
int price = Integer.parseInt(row.get("price"));
int quantity = Integer.parseInt(row.get("quantity"));
inventory.add(new Product(name, price, quantity));
}
}Parse to the required type inside the step definition. Keep the Gherkin table simple (strings only) — adding type metadata to the table itself makes it harder to read.
Verifying with data tables
Data tables aren't just for setup — you can also use them in Then steps to express expected output:
Then the order summary should show:
| item | quantity | price |
| Laptop | 1 | 999 |
| Keyboard | 2 | 49 |@Then("the order summary should show:")
public void orderSummaryShouldShow(DataTable expected) {
List<Map<String, String>> expectedRows = expected.asMaps();
List<Map<String, String>> actualRows = orderPage.getSummaryRows();
assertEquals(expectedRows, actualRows);
}The DataTable's built-in diff() method also works for comparison and produces a readable diff on failure, but manually constructing and comparing lists gives you more control over the assertion message.
Data tables vs Scenario Outline — choosing the right one
Data Table
One scenario execution
Passes structured data INTO a step
Used for setup: create users, load config
Used for verification: assert table of results
Table is part of a single step
Data is input to the step's logic
Use when a step needs a set of items
Scenario Outline
Multiple independent executions
Replaces placeholders across the whole scenario
Each row tests the same behaviour with different data
Each row gets its own pass/fail in the report
Examples table drives execution count
Data drives the scenario parameters
Use when a behaviour needs testing with N inputs
The simplest decision rule: if the data rows represent the input to a single action (creating 3 users in one Given), use a data table. If the data rows represent separate test runs of the same behaviour (login with 5 credential combinations), use Scenario Outline.
⚠️ Common mistakes
- Forgetting the colon after the step text. The step text must end with
:for Cucumber to recognise that a data table follows.Given the following users exist(no colon) will not parse the table as an argument — it will attempt to match a step definition that takes no parameters. - Using the wrong
DataTablemethod.asMaps()on a table without a header row returns garbled data (the first data row becomes the key names). UseasLists()for headerless tables or add a header. - Putting data in the step text instead of a table.
Given a user named "Alice" with email "alice@test.com" and role "admin"works for one user but doesn't scale to 5. When a step needs structured multi-valued input, a data table is the right tool. - Mixing data types.
DataTable.asMaps()returnsMap<String, String>— parsing"true"as a boolean or"30"as an integer must happen in the step definition. Don't assume the type conversion is automatic.
🎯 Practice task
Use a data table to drive bulk setup and verification. 35 minutes.
- Create a
team-management.featurefile with a scenario that uses a data table in theGivenstep to create 3 users. - Write the
@Givenstep definition. Use aList<Map<String, String>>fromasMaps(). For now, print each user to the console to confirm the values are correct. - Add a
Thenstep that uses a data table to verify expected output — for example, a summary of user names. In the step definition, build the expected list from the data table and assert it against a hardcoded actual list (we'll wire real assertions to the app in later lessons). - Add a single-column data table step:
Given the following products are in the cart:with 3 product names. UseasList()in the step definition and print the list. - Stretch: create a step definition that receives a
DataTableand maps it to a list of a custom POJO class (e.g.,Userwithname,email,rolefields) usingdataTable.asMaps()and Java streams. This gives you typed access without parsing each field manually.
Next lesson: tags for organising and selectively running scenarios.