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:
Variable | Description |
---|---|
__arg | The argument passed to call or callonce |
__loop | Current 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]'
- 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:
Pattern | Use Case | Example |
---|---|---|
call read('feature') | Execute feature dynamically | call read('login.feature') |
read('file.json') | Load static data | read('test-data.json') |
read('file.js') | Load function definitions | read('utilities.js') |
def x = call read() | Isolated scope, get result | def result = call read('setup.feature') |
call read() | Shared scope, merge variables | call 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
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'
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:
- Combine calling with data-driven patterns: Data-Driven Tests
- Generate dynamic test scenarios: Dynamic Scenarios