Skip to main content

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());
}
"""
Java vs JavaScript

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

Aspectread()call
PurposeLoad file contentsExecute feature or function
ReturnsFile data (JSON, JS, text, XML)Execution result or function return value
ExecutionNone (just reads)Runs the feature/function
Use withData files, function definitionsFeatures, 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 step
  • call 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 not data1.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

NEVER Commit karate.stop()

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