Skip to main content

REUSABILITY

Calling Features

Reuse authentication flows, test data setup, and common operations across feature files using call, callonce, and JavaScript functions.

Benefits of Feature Calling

  • Authentication reuse: Login once, use tokens across all tests
  • Setup abstraction: Hide complex setup in dedicated feature files
  • Data-driven flows: Pass parameters to parameterize feature execution
  • Maintainability: Update common logic in one place
  • Shared scope: Variables from called features become available to caller

Simple Feature Calling

Call other feature files to reuse common operations:

Feature: Basic feature calling

Scenario: Reuse authentication feature
* def loginResult = call read('login.feature')
* def token = loginResult.authToken

Given url baseUrl
And path 'api/users'
And header Authorization = 'Bearer ' + token
When method get
Then status 200

The called feature returns all its variables in an envelope. Access them by name from the result.

Passing Parameters

Pass data to called features using a single JSON object:

Feature: Calling with parameters

Scenario: Pass credentials to login feature
* def authResult = call read('login.feature') { username: 'admin', password: 'secret123' }
* def token = authResult.token

Given url baseUrl
And path 'protected/resource'
And header Authorization = 'Bearer ' + token
When method get
Then status 200

The called feature accesses parameters using __arg or by variable name directly.

Default Values

Use karate.get() in called features to provide defaults for optional parameters:

Feature: Default values example

# Called feature: user-setup.feature
@ignore
Scenario: Setup with optional parameters
* def username = karate.get('__arg.username', 'testuser')
* def role = karate.get('__arg.role', 'user')
* def email = karate.get('__arg.email', 'default@test.com')

Given url baseUrl
And path 'users'
And request { username: username, role: role, email: email }
When method post
Then status 201

Built-in Variables

Access call arguments and loop index using built-in variables:

VariableDescription
__argThe argument passed to call or callonce
__loopCurrent iteration index when called with array (starts at 0)
Feature: Built-in variables

Scenario: Call with array for data-driven execution
* def users = [
{ name: 'User1', role: 'admin' },
{ name: 'User2', role: 'user' }
]
* def results = call read('create-user.feature') users
* match results == '#[2]'
* match each results contains { userId: '#number' }

# Called feature: create-user.feature
@ignore
Scenario: Create single user
* def userData = __arg
* def index = __loop
* print 'Creating user', userData.name, 'at index', index

Given url baseUrl
And path 'users'
And request userData
When method post
Then status 201
* def userId = response.id

Tag Selectors

Call specific scenarios by tag:

Feature: Tag-based calling

Scenario: Call specific scenario by tag
# Call only @setup scenarios
* call read('common-setup.feature@setup')

# Call scenario by name
* call read('user-workflows.feature@name=createUser')

Given url baseUrl
When method get
Then status 200

Safe Data Passing with copy

Use copy to pass clones and prevent mutation:

Feature: Copy for immutability

Scenario: Prevent data mutation
* def originalData = { id: 1, items: ['item1', 'item2'] }
* def result = call read('modify-data.feature') (copy originalData)

# Original unchanged
* match originalData.items == '#[2]'

# Result modified
* match result.modifiedData.items == '#[3]'
Key Points
  • Called features receive all variables from the caller
  • Changes in called features don't affect caller (unless using shared scope)
  • Only variables defined with def are returned in the result envelope
  • Objects are passed by reference - use copy to clone
  • Only one argument allowed - use objects for multiple values

JavaScript Functions

Define and call reusable JavaScript functions:

Feature: JavaScript function calling

Background:
* def calculateTotal =
"""
function(items) {
var sum = 0;
for (var i = 0; i < items.length; i++) {
sum += items[i].price * items[i].quantity;
}
return { subtotal: sum, tax: sum * 0.08, total: sum * 1.08 };
}
"""

Scenario: Use JavaScript function
* def order = { items: [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
]}
* def totals = call calculateTotal order.items

* match totals.subtotal == 250
* match totals.total == 270

Direct Function Calls

Call functions directly with multiple arguments:

Feature: Direct function calls

Background:
* def multiply = function(a, b) { return a * b }
* def formatName = function(first, last) { return first + ' ' + last }

Scenario: Call with multiple arguments
* def result = multiply(5, 10)
* match result == 50

* def fullName = formatName('John', 'Doe')
* match fullName == 'John Doe'

callonce for Expensive Operations

Use callonce in Background to execute setup only once per feature:

Feature: One-time setup with callonce

Background:
# Executed only once for entire feature
* def authData = callonce read('expensive-auth.feature')
* def token = authData.token

Scenario: First test
Given url baseUrl
And path 'users'
And header Authorization = 'Bearer ' + token
When method get
Then status 200

Scenario: Second test
# authData and token still available, no re-execution
Given url baseUrl
And path 'products'
And header Authorization = 'Bearer ' + token
When method get
Then status 200

Perfect for expensive operations like:

  • Authentication token generation
  • Database initialization
  • Test data creation
  • Environment setup

call vs read()

Different tools for different purposes:

PatternUse CaseExample
call read('feature')Execute feature dynamicallycall read('login.feature')
read('file.json')Load static dataread('test-data.json')
read('file.js')Load function definitionsread('utilities.js')
def x = call read()Isolated scope, get resultdef result = call read('setup.feature')
call read()Shared scope, merge variablescall read('common-setup.feature')
Feature: call vs read patterns

Scenario: Different usage patterns
# read() for static data
* def testData = read('test-users.json')
* def template = read('request-template.json')

# call read() for dynamic execution
* def loginResult = call read('login.feature')
* def setupResult = call read('setup.feature') { env: 'test' }

# read() for function definitions, call for execution
* def utils = read('utilities.js')
* def processed = call utils.processData testData

Advanced: Java Interop

Call Java code from Karate tests:

Feature: Java interop

Background:
* def encodeAuth =
"""
function(user, pass) {
var AuthUtils = Java.type('com.example.AuthUtils');
return AuthUtils.encodeBasicAuth(user, pass);
}
"""

Scenario: Use Java utility
* def authHeader = call encodeAuth 'admin', 'secret'

Given url baseUrl
And path 'secure/api'
And header Authorization = authHeader
When method get
Then status 200

Advanced: karate.callSingle()

Execute features globally once across all test runs using karate.callSingle() in karate-config.js:

Feature: Using global authentication

Scenario: Use globally cached auth
# authToken available from karate-config.js
Given url baseUrl
And path 'user/profile'
And header Authorization = 'Bearer ' + authToken
When method get
Then status 200

Use karate.callSingle() for:

  • Global authentication across all features
  • One-time environment setup
  • Expensive resource initialization
  • Database seeding that spans entire test suite
CallSingle Parallel Execution Restrictions

When using karate.callSingle() or callonce in karate-config.js, only return pure JSON data (or primitives like strings, numbers).

Problematic patterns:

  • ❌ Returning Java objects: new MyCustomClass()
  • ❌ Returning JavaScript functions: function() { ... }
  • ❌ Returning complex objects with circular references

Why: These can cause issues in parallel execution due to caching and serialization across threads.

Safe alternatives:

  • ✅ Return JSON: { token: '...', userId: 123 }
  • ✅ Return string: 'auth-token-value'
  • ✅ Return number: 12345

Example:

// ✅ Good - pure JSON
var result = karate.callSingle('auth.feature');
config.authToken = result.token; // Simple string

// ❌ Avoid - complex objects
var handler = karate.callSingle('setup.feature').customHandler; // Function!

See Java Function References if you need to reuse Java functions.

Calling @ignore Tagged Scenarios

Features or scenarios marked with @ignore are skipped during normal execution but can still be called using call or callonce:

Feature: Using @ignore for helpers

@ignore
Scenario: Login helper
# This won't run as a test, but can be called
Given url authUrl
And request { username: '#(username)', password: '#(password)' }
When method post
Then status 200
* def token = response.authToken

Scenario: Use login helper
* def credentials = { username: 'admin', password: 'secret' }
* def result = call read('@ignore')
* def authToken = result.token
* match authToken == '#string'
@ignore Tag Behavior

The @ignore tag skips scenarios during test execution but they remain callable. This is useful for creating reusable helper scenarios without them running as independent tests.

Next Steps

Master feature calling and continue with: