ADVANCED
Best Practices
Build maintainable, scalable test suites with proven patterns for code reuse, common utilities, and strategies to avoid common pitfalls.
Common Utilities
Karate leverages Java interop for common utility functions instead of building them in. Create reusable utilities once and share across all tests.
Essential Utility Functions
// UUID generation
* def uuid = function(){ return java.util.UUID.randomUUID() + '' }
* def orderId = uuid()
* def correlationId = uuid()
// Current timestamp (milliseconds since epoch)
* def timestamp = function(){ return java.lang.System.currentTimeMillis() + '' }
* def now = timestamp()
// Formatted date strings
* def getDate =
"""
function(pattern) {
var SimpleDateFormat = Java.type('java.text.SimpleDateFormat');
var sdf = new SimpleDateFormat(pattern || 'yyyy-MM-dd');
return sdf.format(new java.util.Date());
}
"""
* def today = getDate('yyyy-MM-dd')
* def timestamp = getDate('yyyy-MM-dd HH:mm:ss')
// Random numbers (0 to max-1)
* def randomInt = function(max){ return Math.floor(Math.random() * max) }
* def randomId = randomInt(10000)
// Sleep/wait
* def sleep = function(ms){ java.lang.Thread.sleep(ms) }
* eval sleep(2000)
Organizing Utility Functions
Create a centralized utility file for project-wide functions:
# File: src/test/java/common-utils.feature
@ignore
Feature: Common utility functions
Scenario: Utility library
* def uuid = function(){ return java.util.UUID.randomUUID() + '' }
* def timestamp = function(){ return java.lang.System.currentTimeMillis() + '' }
* def randomInt = function(max){ return Math.floor(Math.random() * max) }
* def sleep = function(ms){ java.lang.Thread.sleep(ms) }
* def formatDate =
"""
function(pattern) {
var SimpleDateFormat = Java.type('java.text.SimpleDateFormat');
var sdf = new SimpleDateFormat(pattern);
return sdf.format(new java.util.Date());
}
"""
For complex utilities, use Java classes with static methods instead of JavaScript. Java is easier to debug, maintain, and provides better IDE support. Reserve JavaScript for simple, inline transformations.
Code Reuse Patterns
Shared Scope vs Isolated Scope
# Shared scope - functions become global variables
Scenario: Use shared scope utilities
* call read('common-utils.feature')
* def orderId = uuid()
* def now = timestamp()
* match orderId == '#string'
# Isolated scope - functions namespaced in object
Scenario: Use isolated scope utilities
* def utils = call read('common-utils.feature')
* def orderId = utils.uuid()
* def now = utils.timestamp()
* match orderId == '#string'
Use shared scope when:
- Functions are used frequently throughout a feature
- You want concise, direct function calls
- Functions have unique, non-conflicting names
Use isolated scope when:
- You want explicit namespacing
- Avoiding variable name conflicts
- Functions are used sparingly
File Path Prefixes
# classpath: - Project-wide files (recommended for shared resources)
* def sharedUtils = read('classpath:common/utils.js')
* def commonData = read('classpath:data/default-user.json')
# Relative path - Files in same package as feature file
* def localData = read('test-data.json')
* def helper = read('../helpers/validator.js')
# this: - Files relative to called feature (not caller)
* def result = call read('auth-flow.feature')
# Inside auth-flow.feature:
* def config = read('this:auth-config.json') # Relative to auth-flow.feature
# file: - Absolute paths (avoid in CI, useful for local dev)
* def generated = read('file:target/generated-data.json')
Best practice: Use classpath:
for all shared, reusable files. Use relative paths only for co-located test data.
Function Library Pattern
For teams with many utilities, create a modular library:
src/test/java/
├── karate-config.js
├── common/
│ ├── auth-utils.feature
│ ├── data-generators.feature
│ ├── validators.feature
│ └── http-helpers.js
├── data/
│ ├── valid-users.json
│ └── test-products.csv
└── features/
└── api/
└── users.feature
# Load only what you need
Background:
* def auth = call read('classpath:common/auth-utils.feature')
* def generate = call read('classpath:common/data-generators.feature')
Scenario: Create user with generated data
* def user = generate.randomUser()
* def token = auth.getAdminToken()
call vs read
Understanding the distinction prevents common mistakes.
Key Differences
Aspect | read() | call |
---|---|---|
Purpose | Load file contents | Execute feature or function |
Returns | File data (JSON, JS, text, XML) | Execution result or function return value |
Execution | None (just reads) | Runs the feature/function |
Use with | Data files, function definitions | Features, loaded functions |
Common Patterns
# read() - Load data or function definitions
* def userData = read('user-data.json') # Load JSON
* def validator = read('validators.js') # Load function definition
* def template = read('request-template.xml') # Load XML
# call - Execute features or functions
* def result = call read('auth-flow.feature') # Load AND execute
* def token = call validator userData # Call loaded function
* call read('cleanup.feature') # Execute feature
# Common pattern: read() + call
* def myFunction = read('my-function.js') # Load function
* def result = call myFunction { input: 'data' } # Execute function
* def result = myFunction({ input: 'data' }) # Or invoke directly
When to Use Each
Use read()
when:
- Loading test data (JSON, CSV, XML)
- Loading function definitions for later use
- Reading templates or text files
- You need the file contents, not execution results
Use call
when:
- Executing another feature file
- Running a function with arguments
- You need the execution result
Combine both when:
call read('file.feature')
- Load and execute in one stepcall read('file.js')
- Load and invoke function
Data Organization
Test Data Structure
src/test/java/
├── data/
│ ├── users/
│ │ ├── valid-admin.json
│ │ ├── valid-user.json
│ │ └── invalid-users.json
│ ├── products/
│ │ └── sample-catalog.json
│ └── common/
│ └── test-config.json
└── features/
├── users/
│ └── user-api.feature
└── products/
└── product-api.feature
Best practices:
- Group test data by domain or feature
- Use descriptive names:
valid-admin.json
notdata1.json
- Co-locate data with tests when unique to one feature
- Centralize shared data in
data/common/
Configuration Management
// karate-config.js - Keep lean
function fn() {
var env = karate.env || 'dev';
var config = {
baseUrl: 'http://localhost:8080',
adminUser: 'admin',
timeout: 10000
};
if (env === 'qa') {
config.baseUrl = 'https://qa.example.com';
} else if (env === 'prod') {
config.baseUrl = 'https://api.example.com';
config.timeout = 5000;
}
return config;
}
Configuration best practices:
- Keep
karate-config.js
minimal (affects startup performance) - Use environment variables for sensitive data
- Load heavy initialization with
karate.callSingle()
- Avoid complex logic in config
Common Pitfalls
Pass-By-Reference Issues
JavaScript objects are passed by reference. Mutations affect the original.
# Problem: Modifying shared data
* def originalUser = { id: 1, name: 'John', role: 'admin' }
* def modifiedUser = originalUser
* set modifiedUser.role = 'user'
# Both are changed!
* match originalUser.role == 'user'
* match modifiedUser.role == 'user'
# Solution: Deep copy with karate.copy()
* def originalUser = { id: 1, name: 'John', role: 'admin' }
* def modifiedUser = karate.copy(originalUser)
* set modifiedUser.role = 'user'
# Original unchanged
* match originalUser.role == 'admin'
* match modifiedUser.role == 'user'
Always use karate.copy()
when:
- Modifying shared test data
- Creating variations of base objects
- Working with data in loops
- Testing different scenarios with same base data
URL Resets in Called Features
The url
is reset when calling other features, which can cause unexpected failures.
# Problem: URL doesn't persist
Feature: Main test
Background:
* url 'https://api.example.com'
Scenario: Call another feature
* call read('helper.feature') # URL resets inside helper.feature
* path 'users' # This still uses original URL
* method get
# Solution: Use configure url
Feature: Main test
Background:
* configure url = 'https://api.example.com' # Persists across calls
Scenario: Call another feature
* call read('helper.feature') # URL configuration persists
* path 'users'
* method get
Background Variable Scope
Background runs before each scenario. Variables are reset between scenarios.
Feature: Variable scope
Background:
* def counter = 0
* def user = { name: 'John' }
Scenario: First test
* set counter = counter + 1
* match counter == 1
Scenario: Second test
# counter is reset to 0, not 1
* match counter == 0
# user is fresh, not modified from previous scenario
* match user.name == 'John'
For shared state across scenarios:
- Use Java static variables
- Use
karate.callSingle()
for one-time setup - Design scenarios to be independent
karate.stop() Debug Warning
karate.stop(port)
pauses test execution until a socket connection is made to the specified port. This is ONLY for debugging UI tests. If committed, tests will hang indefinitely in CI/CD pipelines.
Always remove karate.stop()
calls before committing code.
# Debug mode only - DO NOT COMMIT
Scenario: Debug browser automation
* if (karate.env == 'debug') karate.stop(9515)
* driver 'https://example.com'
# Connect debugger to localhost:9515 to pause here
Large Number Precision
JavaScript numbers lose precision for very large integers. Use strings or Java BigDecimal.
# Problem: Precision loss
* def largeId = 9007199254740993
* match largeId == 9007199254740992 # Passes incorrectly!
# Solution: Use strings for large IDs
* def largeId = '9007199254740993'
* match largeId == '9007199254740993'
# Solution: Use BigDecimal for calculations
* def BigDecimal = Java.type('java.math.BigDecimal')
* def price = new BigDecimal('99999999.99')
* def tax = price.multiply(new BigDecimal('0.08'))
Anti-Patterns
Hard-Coded URLs
# ❌ Bad
Scenario: Get user
Given url 'https://qa.example.com/api/v1'
# ✅ Good
Background:
* url baseUrl # From karate-config.js
Scenario: Get user
Given path 'api/v1'
Duplicate Test Data
# ❌ Bad - Repeated in every test
Scenario: Create user
* def user = { name: 'John', email: 'john@example.com', role: 'admin' }
Scenario: Update user
* def user = { name: 'John', email: 'john@example.com', role: 'admin' }
# ✅ Good - Centralized
Background:
* def validUser = read('classpath:data/users/admin-user.json')
Scenario: Create user
* def user = karate.copy(validUser)
Complex Logic in Features
# ❌ Bad - Complex JS in feature
* def complexCalculation =
"""
function(data) {
var result = {};
for (var i = 0; i < data.length; i++) {
var item = data[i];
if (item.type === 'A') {
result[item.id] = item.value * 1.5;
} else if (item.type === 'B') {
result[item.id] = item.value * 0.8;
}
// ... 50 more lines
}
return result;
}
"""
# ✅ Good - Use Java for complex logic
* def Calculator = Java.type('com.mycompany.Calculator')
* def result = Calculator.calculate(data)
Bloated karate-config.js
// ❌ Bad - Heavy initialization in config
function fn() {
var config = { baseUrl: 'http://localhost:8080' };
// Expensive operations slow down EVERY scenario
config.token = karate.call('classpath:auth/get-token.feature').authToken;
config.testData = karate.read('classpath:data/large-dataset.json');
config.users = karate.call('classpath:setup/create-users.feature').users;
return config;
}
// ✅ Good - Use callSingle for expensive operations
function fn() {
var config = { baseUrl: 'http://localhost:8080' };
return config;
}
// In feature files:
* def token = callonce read('classpath:auth/get-token.feature')
Not Using classpath: for Shared Files
# ❌ Bad - Fragile relative paths
* def utils = read('../../common/utils.js')
* def data = read('../../../data/test-data.json')
# ✅ Good - Absolute classpath references
* def utils = read('classpath:common/utils.js')
* def data = read('classpath:data/test-data.json')
Performance Tips
Use callSingle for Expensive Setup
# Setup runs once per feature (or JVM), result cached
* def authToken = callonce read('classpath:auth/get-admin-token.feature')
* def testData = callonce read('classpath:setup/seed-database.feature')
# Use cached results in all scenarios
Scenario: Test with auth
Given header Authorization = 'Bearer ' + authToken.token
Scenario: Test with data
* def user = testData.users[0]
Parallel Execution Best Practices
# Ensure scenarios are independent
Feature: Parallel-safe tests
Scenario: Test 1
# Generate unique data per scenario
* def uniqueId = java.util.UUID.randomUUID() + ''
* def user = { id: '#(uniqueId)', name: 'User-' + uniqueId }
Scenario: Test 2
# No shared state with Scenario 1
* def uniqueId = java.util.UUID.randomUUID() + ''
* def user = { id: '#(uniqueId)', name: 'User-' + uniqueId }
Minimize File I/O
# ❌ Bad - Reading same file repeatedly
Scenario Outline: Test with data
* def data = read('test-data.json') # Read on every iteration
* def item = data.items[<index>]
Examples:
| index |
| 0 |
| 1 |
| 2 |
# ✅ Good - Read once in Background
Background:
* def testData = read('test-data.json')
Scenario Outline: Test with data
* def item = testData.items[<index>]
Examples:
| index |
| 0 |
| 1 |
| 2 |
Next Steps
- Explore utility methods: Karate Object API
- Integrate Java code: Java API
- Manage test lifecycle: Hooks
- Optimize execution: Parallel Execution
- Structure projects: Project Structure