Variables and Expressions in Karate

8 min read

Every Karate scenario is a short JavaScript runtime. Variables hold strings, numbers, booleans, JSON objects, and arrays. Expressions compute new values from existing ones. Embedded expressions inject variables into JSON bodies and header strings. This is the plumbing that turns static feature files into dynamic, realistic tests — and the building block for everything else in this chapter.

Defining variables with def

def creates a variable. The value can be any JSON-compatible type or any JavaScript expression:

* def name = 'Alice'
* def statusCode = 200
* def isActive = true
* def user = { name: 'Alice', email: 'alice@test.com', role: 'admin' }
* def tags = ['admin', 'tester']

Variables are scoped to the scenario. A def in Scenario 1 is not visible in Scenario 2. A def in Background is re-evaluated for every scenario — it's not shared state.

JavaScript expressions in def

Karate evaluates the right-hand side of every def as JavaScript. That means any JavaScript expression is valid:

* def timestamp = java.lang.System.currentTimeMillis()
* def uniqueEmail = 'test-' + timestamp + '@test.com'
* def randomId = Math.floor(Math.random() * 10000)
* def upperName = 'alice'.toUpperCase()
* def itemCount = [1, 2, 3].length

java.lang.System.currentTimeMillis() calls into the JVM directly — Karate has a bridge to Java that lets you call any Java class. This is the standard way to generate unique values for test data that won't collide across parallel runs.

Embedded expressions — #(variable)

To use a variable inside a JSON object or a string, use the #(...) syntax. Karate replaces the placeholder with the variable's value at runtime:

* def email = 'alice@test.com'
* def role = 'admin'
* def body = { name: 'Alice', email: '#(email)', role: '#(role)' }
Given request body

The resulting request body is { "name": "Alice", "email": "alice@test.com", "role": "admin" }. The markers are substituted before the request is sent.

Embedded expressions in strings work the same way:

* def authToken = 'abc123xyz'
* header Authorization = 'Bearer #(authToken)'

This produces Authorization: Bearer abc123xyz. No string concatenation needed — the #(...) marker handles it.

Embedded expressions in JSON bodies

You can mix literal values and variable references in a single JSON body:

* def userId = 42
* def amount = 99.99
* def body = { userId: '#(userId)', amount: '#(amount)', currency: 'GBP', confirmed: true }
Given request body

userId and amount come from variables; currency and confirmed are literals. Karate substitutes only the #(...) markers.

For numbers, Karate inserts the numeric value (not a string) when the variable holds a number. { id: '#(userId)' } with userId = 42 produces { "id": 42 }, not { "id": "42" }.

Dynamic variables with karate.set() and karate.get()

When a variable name is computed at runtime — for example, in a loop — you can't use * def because the name is a string. Use karate.set() and karate.get() instead:

* karate.set('dynamicVar', response.id)
* def id = karate.get('dynamicVar')

This is unusual in normal feature files but common in called feature files that process collections or in utility scripts.

The table keyword — inline tabular data

table creates a list of objects from an inline table, without a CSV file:

* table users
  | name    | email            | role    |
  | 'Alice' | 'alice@test.com' | 'admin' |
  | 'Bob'   | 'bob@test.com'   | 'tester'|
 
* def firstUser = users[0]
And match firstUser.name == 'Alice'

users is now an array of objects: [{ name: 'Alice', email: 'alice@test.com', role: 'admin' }, ...]. Each column header becomes a field name; each row is one object. Use table for small inline datasets; use CSV files (next lesson) for larger ones.

Variable types at a glance

⚠️ Common mistakes

  • Using #(variable) in a match assertion. Embedded expressions are for building request data, not for comparing response data. match response.name == '#(name)' does NOT match if response.name equals the value of name — it literally checks for the string #(name). To assert equality, write match response.name == name (no hash, no parens).
  • Expecting def to update across scenarios. A def in Scenario 1 does not affect Scenario 2. If you define * def userId = 42 in Scenario 1 and try to reference userId in Scenario 2 without redefining it, you get null. Use Background for values all scenarios need, or redefine per scenario.
  • Forgetting that table values are strings unless quoted. In a table, every cell value is evaluated as a Karate expression. | Alice | (without quotes) is treated as a variable reference and will be null if Alice is not defined. | 'Alice' | (with quotes) is the string literal Alice. Always quote string values in table cells.

🎯 Practice task

Build and use dynamic test data in a real scenario. 30–40 minutes.

  1. In your Karate project, create a scenario that defines * def timestamp = java.lang.System.currentTimeMillis() and * def uniqueEmail = 'user-' + timestamp + '@test.com'. Print both with * print uniqueEmail. Run and confirm the email is unique each time.
  2. Build a request body using embedded expressions: * def body = { name: 'Test User', email: '#(uniqueEmail)', role: 'tester' }. POST it to JSONPlaceholder /users. Assert status 201. Confirm Karate substituted the email correctly by printing response.
  3. Use table to define three users inline. Access users[0].name and users[2].role with match assertions. Confirm the types are strings (not nulls).
  4. Write a header using an embedded expression: * def token = 'fake-token-123', then * header Authorization = 'Bearer #(token)'. Run any GET and confirm the header appears in the Karate HTML report's request detail.
  5. Prove variable isolation. Define * def marker = 'scenario-1' in Scenario 1. In Scenario 2, attempt * print marker without redefining it. Confirm the output is null — this is the expected behaviour and proves scenarios are isolated.
  6. Stretch: use karate.set('dynamicKey', response.id) after a POST, then * def savedId = karate.get('dynamicKey'). Assert savedId == '#number'. This is the same as def savedId = response.id but shows the dynamic API — useful in looping utility features.

Next lesson: reading test data from external JSON and CSV files — keeping test data out of feature files and making it reusable across scenarios.

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