Data-Driven API Testing in Karate DSL: Scenario Outline with External Data Sources
Karate DSL's Scenario Outline with Examples tables lets you drive one API test from external CSV or JSON files. This guide shows a complete worked example, covers parallel execution, data transformation, and the limits you'll hit at scale.
05 Aug 2026, 03:23 UTC

The Problem: Repeating Test Logic for Every Data Variation
API tests often need to validate the same endpoint against dozens of input combinations — different user roles, payload permutations, or boundary values. Copy-pasting scenarios creates maintenance debt: a schema change means updating every duplicate. Karate DSL solves this with Scenario Outline and Examples tables that accept external CSV or JSON files, turning one scenario into N iterations without code duplication.
How It Works: One Scenario, Many Data Rows
A Scenario Outline defines a template with placeholders (e.g., #(name)). The Examples row supplies a List — each element becomes a separate test iteration. Karate evaluates the Examples cell as a Karate expression, so you can inline a read() call or reference a variable holding parsed data.
Key terms:
- Scenario Outline: Gherkin-style template executed once per data row.
- Examples: The data source; must evaluate to a
List(array of maps or objects). - read(): Built-in function that loads
classpath:orfile:resources and parses CSV/JSON automatically.
Worked Example: POST /users with CSV Data
Create a Maven or Gradle project with Karate 1.4.0:
// pom.xml dependencies
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-junit5</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-apache</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
Add src/test/resources/users.csv:
name,email,age
Alice,[contact removed],30
Bob,[contact removed],25
Carol,[contact removed],35
Create src/test/java/users.feature:
Feature: Create user accounts
Background:
* url baseUrl + '/api/users'
Scenario Outline: create user with CSV data
Given request { name: '#(name)', email: '#(email)', age: #(parseInt(age)) }
When method post
Then status 201
And match response.id != null
Examples:
| read('classpath:users.csv') |
Create a JUnit 5 runner src/test/java/UsersTest.java:
package com.example;
import com.intuit.karate.junit5.Karate;
class UsersTest {
@Karate.Test
Karate testUsers() {
return Karate.run("users").relativeTo(getClass());
}
}
Run from the project root (requires Java 11+ and Maven):
mvn test -Dtest=UsersTest
Expected result: Two POST requests (three rows minus header) with distinct payloads. The HTML report at target/karate-reports/karate-summary.html shows three test cases, each displaying its data row.
Using JSON Instead of CSV
JSON files can be an array of objects or an object with an array property. Example src/test/resources/users.json:
[
{ "name": "Dave", "email": "[contact removed]", "age": 40, "role": "admin" },
{ "name": "Eve", "email": "[contact removed]", "age": 28, "role": "user" }
]
Update the feature:
Examples:
| read('classpath:users.json') |
Placeholders now reference #(role) directly. No parseInt() needed — JSON preserves numeric types.
Transforming Data Before the Examples Table
Sometimes you need derived fields (timestamps, computed values) or filtering. Use karate.map() or karate.filter() in a Background or Scenario before the Outline:
Background:
* def raw = read('classpath:users.json')
* def rows = karate.map(raw, x => ({ ...x, requestId: java.util.UUID.randomUUID().toString() }))
Scenario Outline: create user with generated ID
Given request { name: '#(name)', email: '#(email)', requestId: '#(requestId)' }
When method post
Then status 201
Examples:
| rows |
This runs once per transformed row. The spread operator ...x copies all original fields.
Parallel Execution: Opt-In Concurrency
By default, iterations run sequentially. Enable parallelism two ways:
- Per-scenario: Add
@parallel=trueabove theScenario Outline. - Global: Set
karate.options.parallel = trueinkarate-config.js.
Parallel runs use the JUnit 5 parallel runner. Each iteration gets its own thread but shares the karate-config.js context. Warning: Thread-local state (auth tokens, session IDs) must be re-acquired per iteration or stored in a thread-safe cache (e.g., ConcurrentHashMap via Java interop).
Verify parallelism by checking timestamps in the report — overlapping start times confirm concurrent execution. Total duration should be less than the sequential sum.
Limits and Common Mistakes
1. Examples Must Evaluate to a List
Passing a single Map or scalar throws a cryptic runtime error: expected List but got Map. Always ensure your read() or expression returns an array. For a single-row CSV, wrap it: | karate.toList(read('classpath:single.csv')) |.
2. CSV Values Are Always Strings
Numeric or boolean fields need explicit conversion in the request body: #(parseInt(age)), #(parseFloat(price)), #(active == 'true'). JSON avoids this.
3. Large Data Sets Cause Heap Pressure
Beyond ~10,000 rows, report generation (HTML/JUnit XML) can exhaust JVM heap. Mitigations:
- Split into multiple feature files (e.g.,
users-part1.feature,users-part2.feature). - Use
karate.filter()to subset:| karate.filter(rows, x => x.age > 18) |. - Increase heap:
MAVEN_OPTS=-Xmx2g mvn test.
4. No Dynamic Row Generation from Prior Results
Scenario Outline data is fixed at startup. If you need to generate rows based on a previous API call (e.g., fetch IDs then test each), use karate.call() in a loop inside a regular Scenario instead.
5. Parallel + Shared Config = Race Conditions
If karate-config.js sets a global auth token, parallel iterations may overwrite each other. Solution: move token acquisition into the Background of each feature or use a thread-safe singleton via Java interop.
Verifying the Setup
- Run
mvn test -Dtest=UsersTestand confirm three test cases appear in the console and HTML report. - Add
@parallel=trueto the Outline, re-run, and verify the report's "Duration" column shows overlapping timestamps. - Introduce a malformed CSV (missing header) and confirm the error points to
Examplesevaluation failure — this validates error tracing.
When to Use This Pattern
Use Scenario Outline + external data when:
- Testing the same endpoint with many valid/invalid input combinations.
- Data is maintained by non-developers (CSV in version control).
- You need traceable per-row reporting for flaky-test debugging.
Avoid when:
- Data rows depend on runtime API responses.
- Iterations require complex setup/teardown that differs per row.
- Dataset exceeds practical report size — consider contract testing or property-based testing instead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.