Data-Driven Testing with Scenario Outline and Examples

9 min read

Running the same test with many different inputs is the most common pattern in API testing: valid users, invalid users, edge cases for each field, positive and negative status codes. Without data-driven support you'd write one scenario per case. With Scenario Outline you write one scenario and supply the data in an Examples table. Karate generates a separate test execution per row — automatically, with no extra code.

Scenario Outline and Examples

The syntax is identical to Cucumber:

Feature: User Creation — Validation Cases
 
Scenario Outline: Create user with various roles
  Given url baseUrl
  And path 'users'
  And request { name: '<name>', email: '<email>', role: '<role>' }
  When method post
  Then status <expectedStatus>
 
  Examples:
    | name    | email            | role     | expectedStatus |
    | 'Alice' | 'alice@test.com' | 'admin'  | 201            |
    | 'Bob'   | 'bob@test.com'   | 'tester' | 201            |
    | 'Carol' | 'carol@test.com' | 'viewer' | 201            |

Column names in <angle brackets> are replaced with the row's value at runtime. Each row is a completely independent test execution with its own Karate context — variables from row 1 don't bleed into row 2.

Positive and negative cases in one outline

The real power is mixing valid and invalid inputs in one table:

Scenario Outline: Login — valid and invalid credentials
  Given url baseUrl
  And path 'auth/login'
  And request { email: '<email>', password: '<password>' }
  When method post
  Then status <status>
  And match response.<responseField> == '<expectedValue>'
 
  Examples:
    | email              | password      | status | responseField | expectedValue        |
    | 'admin@test.com'   | 'AdminPass1'  | 200    | token         | '#string'            |
    | 'user@test.com'    | 'UserPass1'   | 200    | token         | '#string'            |
    | 'wrong@test.com'   | 'BadPass'     | 401    | error         | 'Invalid credentials'|
    | ''                 | 'somepass'    | 400    | error         | 'Email is required'  |
    | 'admin@test.com'   | ''            | 400    | error         | 'Password is required'|

Five rows, five independent tests. The same scenario template covers success cases, authentication failures, and missing-field validation. No duplicated steps; the table is the complete specification.

CSV-backed Examples tables

For large datasets, put the rows in a CSV file and reference it in the Examples table:

Scenario Outline: Create users from dataset
  Given url baseUrl
  And path 'users'
  And request { name: '<name>', email: '<email>', role: '<role>' }
  When method post
  Then status 201
 
  Examples:
    | read('test-users.csv') |

Karate reads test-users.csv, interprets the header row as column names, and runs the outline once per data row. The CSV can have dozens or hundreds of rows — the feature file stays identical. This is the cleanest way to keep test data separate from test logic in a data-driven suite.

Calling a feature file with an array — call + array

Scenario Outline works well for tabular data. When you have a JSON array and want to process it programmatically, use call with the array directly:

* def users = read('users.json')
* def results = call read('create-user.feature') users

When call receives an array, it invokes the called feature once per element — passing each element as the argument. The called feature accesses the element's fields via arg or directly as top-level variables (because Karate unpacks the map into the scope):

# create-user.feature — called once per user in the array
Feature: Create User (called feature)
Scenario:
  Given url baseUrl
  And path 'users'
  And request { name: '#(name)', email: '#(email)', role: '#(role)' }
  When method post
  Then status 201
  * def createdId = response.id

name, email, and role come from the array element passed in. call collects the results of each invocation into the results array: results[0].createdId, results[1].createdId, and so on.

Use Scenario Outline when the test is primarily about different inputs to the same HTTP call. Use call with an array when you need more control: conditional logic in the called feature, multi-step flows, or when the data comes from a previous API response rather than a static file.

How data-driven execution works

Step 1 of 5

Examples table or CSV loaded

Karate reads the Examples table inline or the CSV file referenced in the table. Each row becomes one test case.

⚠️ Common mistakes

  • Using <angle brackets> in a regular Scenario instead of Scenario Outline. In a Scenario, <name> is treated as a literal string, not a placeholder. The request body becomes { "name": "<name>" } — the angle brackets are in the payload. Always pair angle-bracket placeholders with Scenario Outline and an Examples table.
  • Forgetting that CSV values are strings. If your expectedStatus column contains 201 and the status keyword expects an integer, the mismatch will cause an error. Karate's Scenario Outline handles the string-to-integer conversion for status and other Karate-native assertions automatically — but if you assign a CSV value to a def variable and then use it in JavaScript arithmetic, you'll need parseInt().
  • Putting too many assertions inside the Scenario Outline. An outline with 15 rows and 10 assertions produces 150 individual assertion points. When something breaks, diagnosing which row and which field failed is harder than it sounds. Keep outlines focused on one API call. If you need to assert on a created resource, write a separate scenario or use call to a dedicated feature.

🎯 Practice task

Write data-driven tests covering positive and negative cases. 35–45 minutes.

  1. Write a Scenario Outline that GETs /users/<id> from JSONPlaceholder and asserts status <status>. Use three rows: id 1 expects 200, id 2 expects 200, id 99999 expects 404. Run all three — confirm the 404 case passes because you're asserting it correctly.
  2. Write a Scenario Outline that POSTs to /users with three different name/email combinations from an inline Examples table. Assert status 201 for all rows. Open the HTML report and find each row listed as a separate scenario.
  3. Create test-users.csv with five user rows. Replace the inline Examples table in step 2 with | read('test-users.csv') |. Confirm all five rows execute as separate tests.
  4. Write a called feature create-user.feature that accepts name and email as inputs, POSTs to /users, and returns createdId. In the main feature, define a small JSON array of two users and use * def results = call read('create-user.feature') users. Assert results[0].createdId == '#number'.
  5. Mixed positive/negative: write a Scenario Outline for a hypothetical /auth/login endpoint with five rows: two valid credential pairs (expect 200), one wrong password (expect 401), one missing email (expect 400), one missing password (expect 400). Even if JSONPlaceholder doesn't have a real auth endpoint, write the outline structure and explain what you'd assert on for each row.
  6. Stretch: read about Karate's @ignore tag. Add @ignore to one row by wrapping that row in a tagged Examples block. Run and confirm the tagged row is skipped.

Next lesson: calling and reusing feature files — the call keyword, callonce for authentication, and organising reusable helpers into a common folder.

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