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.

On this page:

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

Gherkin
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)
When to use karate.lowerCase()

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:

common-utils.feature
@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:

Gherkin
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
Java vs JavaScript

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
Gherkin
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
File Path Guidance
  • 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.json not data1.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:

Gherkin
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'
Always use karate.copy() when:
  • 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:

Gherkin
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:

Gherkin
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'
For shared state across scenarios:
  • Use callonce in 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

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 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:

Gherkin
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:

Gherkin
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:

Gherkin
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:

Gherkin
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:

karate-config.js
// 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:

Gherkin
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:

Readability Over Reuse

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:

Gherkin
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:

Gherkin
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:

Gherkin
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:

Gherkin
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