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') usersWhen 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.idname, 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 aScenario,<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 withScenario Outlineand anExamplestable. - Forgetting that CSV values are strings. If your
expectedStatuscolumn contains201and thestatuskeyword expects an integer, the mismatch will cause an error. Karate's Scenario Outline handles the string-to-integer conversion forstatusand other Karate-native assertions automatically — but if you assign a CSV value to adefvariable and then use it in JavaScript arithmetic, you'll needparseInt(). - 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
callto a dedicated feature.
🎯 Practice task
Write data-driven tests covering positive and negative cases. 35–45 minutes.
- Write a
Scenario Outlinethat GETs/users/<id>from JSONPlaceholder and assertsstatus <status>. Use three rows: id1expects200, id2expects200, id99999expects404. Run all three — confirm the 404 case passes because you're asserting it correctly. - Write a
Scenario Outlinethat POSTs to/userswith three differentname/emailcombinations from an inline Examples table. Assertstatus 201for all rows. Open the HTML report and find each row listed as a separate scenario. - Create
test-users.csvwith five user rows. Replace the inline Examples table in step 2 with| read('test-users.csv') |. Confirm all five rows execute as separate tests. - Write a called feature
create-user.featurethat acceptsnameandemailas inputs, POSTs to/users, and returnscreatedId. In the main feature, define a small JSON array of two users and use* def results = call read('create-user.feature') users. Assertresults[0].createdId == '#number'. - Mixed positive/negative: write a
Scenario Outlinefor a hypothetical/auth/loginendpoint 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. - Stretch: read about Karate's
@ignoretag. Add@ignoreto 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.