Data Tables in Steps

8 min read

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 listed

The 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 DataTable method. asMaps() on a table without a header row returns garbled data (the first data row becomes the key names). Use asLists() 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() returns Map<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.

  1. Create a team-management.feature file with a scenario that uses a data table in the Given step to create 3 users.
  2. Write the @Given step definition. Use a List<Map<String, String>> from asMaps(). For now, print each user to the console to confirm the values are correct.
  3. Add a Then step 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).
  4. Add a single-column data table step: Given the following products are in the cart: with 3 product names. Use asList() in the step definition and print the list.
  5. Stretch: create a step definition that receives a DataTable and maps it to a list of a custom POJO class (e.g., User with name, email, role fields) using dataTable.asMaps() and Java streams. This gives you typed access without parsing each field manually.

Next lesson: tags for organising and selectively running scenarios.

// tip to track lessons you complete and pick up where you left off across devices.