ADVANCED
Best Practices
Build maintainable, scalable test suites with proven patterns for code reuse, common utilities, and strategies to avoid common pitfalls.
On this page:
- Common Utilities - UUID, timestamp, date, random number generation
- Organizing Utilities - Centralized utility files
- Data Organization - Test data structure patterns
- Common Pitfalls - Pass-by-reference, URL resets, variable scope
- Anti-Patterns - Hard-coded URLs, duplicate data, complex logic
- Performance Tips - callonce, parallel execution, file I/O
Common Utilities
Karate doesn't include random-number functions, UUID generators, or date utilities out of the box because it's easy to create them using Java interop. Here are the most commonly needed utilities:
Essential Utility Functions
Feature: Common utilities
Scenario: UUID and timestamp generation
# UUID - Generate a unique identifier for test data
# The '+ ""' converts the Java UUID object to a string
* def uuid = function(){ return java.util.UUID.randomUUID() + '' }
* def orderId = uuid()
* match orderId == '#uuid'
# Timestamp - Get current time in milliseconds since epoch
# Useful for creating unique values or tracking when things happened
* def timestamp = function(){ return java.lang.System.currentTimeMillis() + '' }
* def now = timestamp()
* match now == '#string'
# Random numbers - Generate a number from 0 to max-1
# Math.floor rounds down to get a whole number
* def randomInt = function(max){ return Math.floor(Math.random() * max) }
* def randomId = randomInt(10000)
* assert randomId >= 0 && randomId < 10000
Scenario: Formatted date strings
# Custom date formatting using Java's SimpleDateFormat
# Common patterns: 'yyyy-MM-dd', 'dd/MM/yyyy', 'yyyy-MM-dd HH:mm:ss'
* 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')
* match today == '#regex \\d{4}-\\d{2}-\\d{2}'
Scenario: Case-insensitive string comparison
# Java's equalsIgnoreCase handles locale-aware comparison
# For bulk operations, consider karate.lowerCase() instead
* def equalsIgnoreCase = function(a, b){ return a.equalsIgnoreCase(b) }
* assert equalsIgnoreCase('Hello', 'HELLO') == true
* assert equalsIgnoreCase('Test', 'test') == true
Scenario: Sleep utility
# Pause test execution - use sparingly, prefer retry patterns
# Argument is milliseconds (1000 = 1 second)
* def sleep = function(ms){ java.lang.Thread.sleep(ms) }
* eval sleep(100)
For case-insensitive comparisons across entire JSON objects or when comparing many strings, use karate.lowerCase(object) instead of the equalsIgnoreCase function. It converts all keys and values to lowercase in one operation.
Organizing Utility Functions
For projects with many tests, create a centralized utility file that can be imported into any feature. Mark it with @ignore so it doesn't run as a standalone test:
@ignore
Feature: Common utility functions
Scenario: Utility library
# These functions become available when this feature is called
* 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());
}
"""
Import and use the utilities in your tests. Using call read() with assignment creates an isolated namespace, keeping your test's scope clean:
Feature: Using common utilities
Background:
# Load utilities into a 'utils' namespace
# All functions are now accessible as utils.functionName()
* def utils = call read('classpath:common-utils.feature')
Scenario: Create order with generated data
* def orderId = utils.uuid()
* def createdAt = utils.timestamp()
Given url 'https://jsonplaceholder.typicode.com'
And path 'posts'
And request { title: '#(orderId)', body: 'Test order', userId: 1 }
When method post
Then status 201
For complex utilities, use Java classes with static methods instead of JavaScript. Java is easier to debug, provides better IDE support, and handles edge cases more reliably. Reserve JavaScript for simple, inline transformations.
Data Organization
Test Data Structure
Organize test data by domain (users, products, orders) rather than by feature file. This makes data easier to find and promotes reuse across multiple tests:
src/test/java/
├── data/
│ ├── users/
│ │ ├── valid-admin.json # Complete admin user object
│ │ ├── valid-user.json # Standard user object
│ │ └── invalid-users.json # Array of invalid user variants
│ ├── products/
│ │ └── sample-catalog.json
│ └── common/
│ └── test-config.json # Shared configuration values
└── features/
├── users/
│ └── user-api.feature
└── products/
└── product-api.feature
Feature: Using organized test data
Background:
# Load test data once - available to all scenarios in this feature
* def validUser = read('classpath:data/users/valid-admin.json')
Scenario: Create user from test data
Given url 'https://jsonplaceholder.typicode.com'
And path 'users'
And request validUser
When method post
Then status 201
- Use
classpath:for shared, reusable files (enables moving files without breaking paths) - Use relative paths only for test data that's unique to one feature file
- Use descriptive names:
valid-admin.jsonnotdata1.json
Common Pitfalls
Pass-By-Reference Issues
In JavaScript (and Karate), objects are passed by reference. When you assign an object to a new variable, both variables point to the same object in memory. Changing one changes both:
Feature: Pass-by-reference demonstration
Scenario: Problem - modifying shared data
* def originalUser = { id: 1, name: 'John', role: 'admin' }
# This does NOT create a copy - both variables point to the same object
* def modifiedUser = originalUser
* set modifiedUser.role = 'user'
# Both are changed - this is usually not what you want
* match originalUser.role == 'user'
* match modifiedUser.role == 'user'
Scenario: Solution - deep copy with karate.copy()
* def originalUser = { id: 1, name: 'John', role: 'admin' }
# karate.copy() creates a completely independent clone
* def modifiedUser = karate.copy(originalUser)
* set modifiedUser.role = 'user'
# Original unchanged because we modified a separate copy
* match originalUser.role == 'admin'
* match modifiedUser.role == 'user'
- Modifying test data loaded from a file (the file data is cached and shared)
- Creating variations of a base object for different test cases
- Working with data in loops or data-driven tests
- Any time you need to change an object without affecting the original
URL Resets in Called Features
When you call another feature file, the url setting resets. This catches many beginners by surprise. Use configure url to make it persist across feature calls:
Feature: URL persistence
Background:
# Regular 'url' keyword resets after calling other features
# 'configure url' persists across feature calls
* configure url = 'https://jsonplaceholder.typicode.com'
Scenario: Call another feature
# URL configuration persists into helper.feature
* call read('helper.feature')
# Can continue making requests without re-setting url
* path 'users'
* method get
* status 200
Background Variable Scope
The Background section runs before each scenario, not once per feature. Variables defined in Background are reset to their initial values between scenarios:
Feature: Variable scope demonstration
Background:
# These run fresh before EVERY scenario
* def counter = 0
* def user = { name: 'John' }
Scenario: First test
* set counter = counter + 1
* match counter == 1
Scenario: Second test
# counter is 0 again, not 1 - Background re-ran
* match counter == 0
# user is a fresh object, not the one modified in First test
* match user.name == 'John'
- Use
calloncein Background to run expensive setup once per feature - Use
karate.callSingle()in karate-config.js for global one-time setup - Design scenarios to be independent (recommended for parallel execution)
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 locally. If committed to source control, tests will hang indefinitely in CI/CD pipelines, blocking builds forever.
Always remove karate.stop() calls before committing code.
Large Number Precision
JavaScript can only safely represent integers up to 2^53 - 1 (9,007,199,254,740,991). Larger numbers lose precision silently, causing tests to pass when they shouldn't:
Feature: Large number handling
Scenario: Problem - precision loss
* def largeId = 9007199254740993
# This passes incorrectly! JavaScript rounds the number
* match largeId == 9007199254740992
Scenario: Solution - use strings for large IDs
# Keep large numbers as strings to preserve all digits
* def largeId = '9007199254740993'
* match largeId == '9007199254740993'
Scenario: Solution - use BigDecimal for calculations
# Java's BigDecimal handles arbitrary precision
* def BigDecimal = Java.type('java.math.BigDecimal')
* def price = new BigDecimal('99999999.99')
* def tax = price.multiply(new BigDecimal('0.08'))
* match tax.toString() == '7999999.9992'
Anti-Patterns
Hard-Coded URLs
Hard-coded URLs break when you need to test against different environments (dev, staging, production). Use configuration instead:
Feature: URL management
# Avoid - URL is embedded in the test
Scenario: Hard-coded URL (avoid)
Given url 'https://qa.example.com/api/v1'
And path 'users'
When method get
Then status 200
# Preferred - URL comes from karate-config.js
Scenario: URL from config
# baseUrl is defined in karate-config.js and changes per environment
Given url baseUrl
And path 'api/v1/users'
When method get
Then status 200
Duplicate Test Data
Copying the same test data into multiple scenarios makes maintenance difficult. When the data structure changes, you have to update it everywhere:
Feature: Test data patterns
Background:
# Load once, use everywhere - single source of truth
* def validUser = read('classpath:data/users/admin-user.json')
# Avoid - same data repeated in every scenario
Scenario: Duplicate data (avoid)
* def user = { name: 'John', email: 'john@example.com', role: 'admin' }
# This same object appears in 10 other scenarios...
# Preferred - centralized data, copied when modifications needed
Scenario: Centralized data with copy
# Start with base data, modify only what's different
* def user = karate.copy(validUser)
* set user.name = 'Jane'
Complex Logic in Features
Feature files should be readable, not clever. Move complex logic to Java where it can be properly tested, debugged, and maintained:
Feature: Logic organization
# Avoid - complex JavaScript embedded in feature
Scenario: Complex inline JavaScript (avoid)
* 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;
}
}
return result;
}
"""
# Preferred - complex logic lives in a Java class
Scenario: Java for complex calculations
* def Calculator = Java.type('com.mycompany.Calculator')
* def result = Calculator.calculate(data)
Bloated karate-config.js
Configuration runs before every scenario. Heavy initialization here slows down your entire test suite:
// Avoid - expensive operations run before EVERY scenario
function fnBad() {
var config = { baseUrl: 'http://localhost:8080' };
// These slow down every single scenario
config.token = karate.call('classpath:auth/get-token.feature').authToken;
config.testData = karate.read('classpath:data/large-dataset.json');
return config;
}
// Preferred - minimal config, move expensive operations to features
function fnGood() {
var config = { baseUrl: 'http://localhost:8080' };
return config;
}
Move expensive setup to feature files using callonce:
Feature: Efficient setup
Background:
# callonce runs only once per feature file, caches result
# Much faster than running auth in karate-config.js
* def auth = callonce read('classpath:auth/get-token.feature')
* def token = auth.authToken
Over-Using Feature Reuse
While Karate makes it easy to call other features, excessive reuse can make tests hard to understand:
Don't abstract everything into reusable features. If understanding a test requires reading 5 different feature files, the test is too fragmented. Sometimes a little duplication is better than complex abstraction chains.
Fragile Relative Paths
Relative paths break when files move. Use classpath: for stable references:
Feature: Path patterns
# Avoid - breaks if directory structure changes
Scenario: Relative paths (avoid)
* def utils = read('../../common/utils.js')
* def data = read('../../../data/test-data.json')
# Preferred - works regardless of feature file location
Scenario: Classpath references
* def utils = read('classpath:common/utils.js')
* def data = read('classpath:data/test-data.json')
Performance Tips
Use callonce for Expensive Setup
Authentication, database seeding, and test data creation should run once per feature, not before every scenario:
Feature: Efficient authentication
Background:
# Runs once, result cached for all scenarios in this feature
* def authToken = callonce read('classpath:auth/get-admin-token.feature')
* def testData = callonce read('classpath:setup/seed-database.feature')
Scenario: First test - uses cached auth
Given url 'https://jsonplaceholder.typicode.com'
And path 'users'
And header Authorization = 'Bearer ' + authToken.token
When method get
Then status 200
Scenario: Second test - same cached auth, no re-authentication
* def user = testData.users[0]
* match user.id == '#number'
Parallel Execution Best Practices
Karate runs scenarios in parallel by default. Ensure your tests don't interfere with each other:
Feature: Parallel-safe tests
Scenario: Test 1 - uses unique data
# Generate unique IDs so parallel tests don't conflict
* def uniqueId = java.util.UUID.randomUUID() + ''
* def user = { id: '#(uniqueId)', name: 'User-' + uniqueId }
Given url 'https://jsonplaceholder.typicode.com'
And path 'posts'
And request { title: '#(uniqueId)', body: 'Test', userId: 1 }
When method post
Then status 201
Scenario: Test 2 - completely independent
# No shared state with Test 1 - both can run simultaneously
* def uniqueId = java.util.UUID.randomUUID() + ''
Given url 'https://jsonplaceholder.typicode.com'
And path 'posts'
And request { title: '#(uniqueId)', body: 'Test', userId: 1 }
When method post
Then status 201
Minimize File I/O
Read data files once in Background, not repeatedly in each scenario iteration:
Feature: Efficient file reading
# File is read once, used across all iterations of the Scenario Outline
Background:
* def testData = read('test-data.json')
Scenario Outline: Test with data - efficient
# Access already-loaded data by index
* def item = testData.items[<index>]
* match item.id == '#number'
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
- Reuse code effectively: Calling Features