Skip to main content

ADVANCED

Java API & Utilities

Invoke Karate from Java code, configure comprehensive logging, mask sensitive data, and optimize test execution for CI/CD pipelines.

On this page:

Quick Start with jbang

jbang lets you run Karate tests instantly without Maven, Gradle, or any build configuration. Install jbang once, then run any .feature file directly from the command line. This is ideal for quick experiments, demos, or running tests on machines without a full development environment.

Shell
# Install jbang: https://www.jbang.dev

# Run a single feature file
jbang karate@karatelabs test.feature

# Run with specific environment
jbang karate@karatelabs --env=qa test.feature

# Run with tags
jbang karate@karatelabs --tags=@smoke features/

# Parallel execution
jbang karate@karatelabs --threads=5 features/

jbang Java DSL

For scenarios where you need Java code instead of .feature files, Karate provides a Java DSL (Domain Specific Language). This is useful for quick scripts, one-off API calls, or integrating Karate into existing Java applications. Save the code as javadsl.java and run with jbang javadsl.java:

javadsl.java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS com.intuit.karate:karate-core:RELEASE:all

import com.intuit.karate.*;
import java.util.List;

public class javadsl {
public static void main(String[] args) {
List users = Http.to("https://jsonplaceholder.typicode.com/users")
.get().json().asList();
Match.that(users.get(0)).contains("{ name: 'Leanne Graham' }");
String city = Json.of(users).get("$[0].address.city");
Match.that("Gwenborough").isEqualTo(city);
System.out.println("\n*** second user: " + Json.of(users.get(1)).toString());
}
}
note

Replace RELEASE with a specific Karate version if needed. The Java DSL provides Http, Json, and Match classes for scripting without feature files.

Runner API Basics

The Runner API lets you execute Karate tests from within Java code, giving you full programmatic control. This is the standard approach for running tests in CI/CD pipelines, where you need to specify which tests to run, configure parallel execution, and capture results for reporting.

The key components are:

  • Runner.path() - Specify which feature files or directories to run
  • Results - Object containing pass/fail counts, error messages, and timing data
Java
import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

@Test
void runKarateTests() {
Results results = Runner.path("classpath:features")
.tags("~@ignore")
.parallel(5);

assertEquals(0, results.getFailCount(),
results.getErrorMessages());
}

Advanced Runner Configuration

Chain multiple builder methods to customize test execution. The example below runs tests from two locations, filters by tags, sets the environment to "staging", enables both Cucumber JSON and JUnit XML reports for CI integration, and runs with 10 parallel threads:

Java
@Test
void advancedRunnerConfig() {
Results results = Runner.path("classpath:api", "classpath:smoke.feature")
.tags("@regression", "~@wip")
.karateEnv("staging")
.outputCucumberJson(true)
.outputJunitXml(true)
.reportDir("target/karate-reports")
.parallel(10);

assertTrue(results.getFailCount() == 0,
"Found " + results.getFailCount() + " failures");
}

Runner Builder Methods

MethodDescription
path(String...)Feature files or directories to run
tags(String...)Include/exclude by tags (~@ignore to exclude)
karateEnv(String)Set the Karate environment
systemProperty(String, String)Set a Java system property
parallel(int)Number of threads (must be last)
reportDir(String)Output directory for reports (default: target/karate-reports)
outputJunitXml(boolean)Generate JUnit XML reports for CI
outputCucumberJson(boolean)Generate Cucumber JSON reports
outputHtmlReport(boolean)Enable/disable HTML reports (default: true)
dryRun(boolean)Preview which tests will run without executing
hook(RuntimeHook)Add a custom lifecycle hook

Dry Run Mode

Preview which tests will run without executing them. Useful for reviewing tag coverage:

Java
Results results = Runner.path("classpath:features")
.tags("@smoke")
.dryRun(true)
.parallel(1);

The dry run report shows all features and steps that would execute, including comments, but without actual HTTP calls or time durations.

Invoking Features from Java

Sometimes you need to call a Karate feature file from within Java code - for example, to set up test data before a Selenium UI test, or to verify database state after an operation. Runner.runFeature() executes a single feature and returns all variables defined in that feature as a Map, allowing you to access response, custom variables, or any other data set during execution.

Java
import com.intuit.karate.Runner;
import java.util.Map;
import java.util.HashMap;

public class JavaApiExample {
public static void main(String[] args) {
// Pass variables to the feature
Map<String, Object> vars = new HashMap<>();
vars.put("userId", 1);

// Run feature and get results (true = process karate-config.js)
Map<String, Object> result = Runner.runFeature(
"classpath:features/get-user.feature",
vars,
true
);

// Access variables set in the feature
Object user = result.get("response");
System.out.println("User: " + user);
}
}
tip

Use Runner.runFeature() to integrate Karate API calls into larger test suites (e.g., Selenium tests) for data setup or verification.

karate.callSingle()

When running tests in parallel, karate-config.js executes once per thread, which can cause problems for expensive operations like authentication. If you have 10 parallel threads, your login feature would run 10 times unnecessarily.

karate.callSingle() solves this by guaranteeing code runs exactly once across all threads, with the result cached and shared. This is essential for:

  • Authentication tokens - Login once, share the token across all tests
  • Test data setup - Create a user once, use it everywhere
  • Database seeding - Initialize data once before all tests
karate-config.js
function fn() {
var config = {};

// This runs only once, even with parallel execution
var authResult = karate.callSingle('classpath:auth/get-token.feature');
config.authToken = authResult.token;

return config;
}

callSingle with Arguments

You can pass data to callSingle() using a second parameter. However, since callSingle() caches results by the feature file path, calling the same feature with different arguments would return the same cached result.

To cache different argument combinations separately, add a ?key suffix to the feature path. Each unique key creates a separate cache entry:

karate-config.js
function fn() {
// Different cache entries for admin vs user tokens
var adminToken = karate.callSingle('classpath:auth/get-token.feature?admin',
{ username: 'admin', password: 'admin123' });
var userToken = karate.callSingle('classpath:auth/get-token.feature?user',
{ username: 'user', password: 'user456' });

return { adminToken: adminToken.token, userToken: userToken.token };
}

callSingleCache

During local development, you often restart tests frequently. By default, callSingle() only caches in memory, so every restart triggers a fresh login. This slows down development.

Enable disk caching to persist results between test runs. The cache is automatically invalidated after the specified duration:

karate-config.js
function fn() {
if (karate.env == 'local') {
// Cache for 15 minutes during local development
karate.configure('callSingleCache', { minutes: 15 });
}

var auth = karate.callSingle('classpath:auth/login.feature');
return { authToken: auth.token };
}
warning

callSingle should return pure JSON data. Complex objects like Java instances or JS functions with closures may cause issues in parallel execution.

Logging Configuration

Karate uses Logback for logging. By default, logs go to the console, but you can customize log levels, formats, and destinations by creating a configuration file.

Basic Logback Setup

Create logback-test.xml in src/test/resources/. Karate will automatically detect and use this file. The example below logs Karate messages at DEBUG level (showing HTTP request/response details) while keeping other libraries at INFO:

logback-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<logger name="com.intuit.karate" level="DEBUG"/>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

Environment-Specific Logging

You may want verbose DEBUG logs during local development but quieter INFO logs in CI pipelines. Logback supports conditional configuration using Janino (add it as a dependency). This checks the karate.env system property and adjusts log levels accordingly:

logback-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<if condition='property("karate.env").equals("ci")'>
<then>
<logger name="com.intuit.karate" level="INFO"/>
</then>
<else>
<logger name="com.intuit.karate" level="DEBUG"/>
</else>
</if>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

File and Console Logging

For troubleshooting, you often want full logs saved to a file while keeping console output minimal. This configuration writes all DEBUG logs to target/karate.log but only shows WARN and ERROR on the console:

logback-test.xml
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>target/karate.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>

<logger name="com.intuit.karate" level="DEBUG"/>

<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>

Log Masking for Security

When testing authentication endpoints, logs and HTML reports will contain passwords, tokens, and API keys in plain text. This is a security risk if logs are shared or stored. Karate's HttpLogModifier interface lets you intercept and redact sensitive data before it's written to logs or reports.

Custom Log Modifier

Create a Java class that implements HttpLogModifier. The interface has three methods:

  • enableForUri() - Return true for URLs that need masking (e.g., /login, /auth)
  • request() - Modify request logs (mask passwords before they're logged)
  • response() - Modify response logs (mask tokens in API responses)
Java
public class SecureLogModifier implements HttpLogModifier {

@Override
public boolean enableForUri(String uri) {
// Enable masking for auth endpoints
return uri.contains("/auth") || uri.contains("/login");
}

@Override
public String request(String msg) {
// Mask passwords in request logs
return msg.replaceAll(
"\"password\"\\s*:\\s*\"[^\"]+\"",
"\"password\":\"***MASKED***\""
);
}

@Override
public String response(String msg) {
// Mask tokens in response logs
return msg.replaceAll(
"\"token\"\\s*:\\s*\"[^\"]+\"",
"\"token\":\"***MASKED***\""
);
}
}

Using Log Modifier in Tests

Once you've created the Java class, activate it using configure logModifier. You can do this in individual feature files or globally in karate-config.js. The Java.type() function loads your Java class, and new creates an instance:

Gherkin
Feature: Secure logging

Background:
# Load the Java class and create an instance
* def LogModifier = Java.type('com.example.SecureLogModifier')
* configure logModifier = new LogModifier()

Scenario: Login with masked credentials
Given url authServiceUrl
And path 'login'
And request { username: 'user@test.com', password: 'secret123' }
When method post
Then status 200
# Password is masked in logs and reports
note

Configure authServiceUrl in karate-config.js to point to your authentication service.

Report Verbosity Control

Karate automatically generates HTML reports showing every step, HTTP request, and response. While useful for debugging, these detailed reports can be overwhelming for stakeholders or when reviewing many tests. You can control exactly what appears in reports.

Configure Report Output

The configure report setting accepts two options:

  • showLog - Show/hide HTTP requests and responses in the report (default: true)
  • showAllSteps - Show/hide steps that start with * (default: true)
Gherkin
Feature: Report configuration

Scenario: Detailed reporting
# Full verbose reporting
* configure report = { showLog: true, showAllSteps: true }
* print 'This appears in report'
* def data = 'visible variable'

Scenario: Minimal reporting
* configure report = { showLog: false, showAllSteps: false }
* print 'Only print statements visible'
* def internal = 'hidden from report'

Feature-Level Report Control

For utility features that contain helper functions or setup logic, you can hide the entire feature from reports using the @report=false tag. This keeps reports focused on actual test scenarios:

Gherkin
@report=false
Feature: Utility functions

Scenario: Helper function (hidden from reports)
* def helper = function(x){ return x * 2 }
* def result = helper(5)
* match result == 10

Logging in Tests

Using karate.log()

Use karate.log() to add custom log messages during test execution. Unlike print (which only outputs to console), karate.log() integrates with the Logback logging framework, so messages appear in log files and can be filtered by log level. This is useful for debugging complex scenarios or adding audit trails:

Gherkin
Feature: Test logging

Scenario: Structured logging
* karate.log('Starting user creation test')
* def userData = { name: 'Alice', email: 'alice@test.com' }
* karate.log('User data prepared:', userData)

Given url 'https://jsonplaceholder.typicode.com'
And path 'users'
And request userData
When method post
* karate.log('Response status:', responseStatus)
* karate.log('Created user ID:', response.id)
Then status 201

CI/CD Integration

Pipeline-Friendly Configuration

CI/CD pipelines generate thousands of log lines, making it hard to find failures. This configuration automatically detects when running in CI (via karate.env) and:

  1. Disables print statements to reduce noise
  2. Minimizes report verbosity
  3. Captures only failure information for debugging
karate-config.js
function fn() {
var config = { baseUrl: 'https://jsonplaceholder.typicode.com' };

if (karate.env === 'ci') {
// Minimal logging in CI
karate.configure('printEnabled', false);
karate.configure('report', {
showLog: false,
showAllSteps: false
});

// Capture failures
karate.configure('afterScenario',
function() {
if (karate.info.errorMessage) {
karate.log('FAILED:', karate.info.scenarioName);
karate.log('ERROR:', karate.info.errorMessage);
}
}
);
}

return config;
}

Parallel Execution Stats

The Results object returned by Runner.parallel() contains comprehensive execution statistics. Use these to monitor test health, identify slow tests, and generate custom reports for your CI dashboard:

Java
Results results = Runner.path("classpath:features")
.parallel(5);

System.out.println("Scenarios passed: " + results.getPassCount());
System.out.println("Scenarios failed: " + results.getFailCount());
System.out.println("Total time: " + results.getElapsedTime() + "ms");
System.out.println("Features: " + results.getFeaturesPassed() + "/" +
results.getFeaturesTotal());

if (results.getFailCount() > 0) {
System.err.println("Failures:\n" + results.getErrorMessages());
}

Commonly Needed Utilities

Dynamic Port Numbers

When running tests against a local server, you may need different port numbers for different environments or parallel test runs. Use karate.properties[] to read Java system properties passed via the command line:

karate-config.js
function fn() {
var port = karate.properties['server.port'] || '8080';
var config = {
baseUrl: 'http://localhost:' + port + '/api'
};
return config;
}

Run tests with dynamic port:

Shell
mvn test -Dserver.port=8081

Multiple Functions in One File

Instead of scattering utility functions across many files, you can bundle related functions into a single JavaScript file that returns an object. This pattern keeps utilities organized and makes them easy to import with a single call read():

utils.js
function() {
return {
randomEmail: function() {
return 'user' + Math.floor(Math.random() * 10000) + '@test.com';
},

randomString: function(length) {
var chars = 'abcdefghijklmnopqrstuvwxyz';
var result = '';
for (var i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
},

formatDate: function(date) {
return new Date(date).toISOString().split('T')[0];
}
};
}

Use the utilities in tests:

Gherkin
Feature: Utility functions

Background:
* def utils = call read('classpath:utils.js')

Scenario: Use utility functions
* def email = utils.randomEmail()
* def password = utils.randomString(12)
* def today = utils.formatDate(new Date())
* print 'Generated email:', email
* print 'Password length:', password.length
* print 'Today:', today

Java Function References

Karate can call any Java static method using Java.type(). This is powerful for operations that are complex in JavaScript, like cryptographic hashing, database connections, or custom business logic. The pattern is:

  1. Load the Java class with Java.type('fully.qualified.ClassName')
  2. Call static methods directly on the type, or use new for instance methods
Gherkin
Feature: Java interop

Background:
* def Utils = Java.type('com.example.TestUtils')
* def UUID = Java.type('java.util.UUID')

Scenario: Use Java utilities
* def randomId = UUID.randomUUID().toString()
* def hash = Utils.generateHash('password123')
* def timestamp = Utils.getCurrentTimestamp()
* print 'Random ID:', randomId
* print 'Hash:', hash

When to Use What

Choose the right tool based on your testing needs:

NeedUse ThisWhy
Run tests without build tooljbangZero setup, instant execution
Programmatic test controlRunner APIFull Java integration
Mask passwords/tokensHttpLogModifierSecurity compliance
CI/CD loggingEnvironment configReduce pipeline noise
Performance trackingCustom functionsMonitor bottlenecks
Reusable utilitiesutils.jsDRY principle
Dynamic configurationkarate-config.jsEnvironment switching
Security Best Practice

Always mask sensitive data (passwords, tokens, API keys) in logs and reports. Use HttpLogModifier for production test suites to ensure compliance.

Performance Monitoring

Track Execution Time

While Karate's HTML reports show timing data, sometimes you need custom performance tracking within your tests. This timer utility uses karate.set() and karate.get() to store timestamps, then calculates duration. You can assert that operations complete within expected thresholds:

Gherkin
Feature: Performance monitoring

Background:
* def timer =
"""
{
start: function(name) {
karate.set(name + '_start', Date.now());
},
end: function(name) {
var start = karate.get(name + '_start');
var duration = Date.now() - start;
karate.log(name + ' took ' + duration + 'ms');
return duration;
}
}
"""

Scenario: Profile API calls
* timer.start('users')
Given url 'https://jsonplaceholder.typicode.com'
And path 'users'
When method get
Then status 200
* def usersTime = timer.end('users')

* timer.start('posts')
Given path 'posts'
When method get
Then status 200
* def postsTime = timer.end('posts')

* assert usersTime < 5000
* assert postsTime < 5000

Memory Usage Tracking

For tests that process large datasets, you may want to monitor memory consumption. This function uses Java's Runtime class to calculate used memory. Note that this is an advanced debugging technique - most tests don't need memory monitoring:

Gherkin
Feature: Memory monitoring

Background:
* def memory =
"""
function() {
var runtime = java.lang.Runtime.getRuntime();
var used = runtime.totalMemory() - runtime.freeMemory();
return Math.round(used / 1024 / 1024) + ' MB';
}
"""

Scenario: Memory monitoring
* def initialMemory = memory()
* print 'Initial memory:', initialMemory

# Load large dataset
* def largeData = read('classpath:large-file.json')
* def afterLoadMemory = memory()
* print 'After load:', afterLoadMemory

# Process data
* def processed = karate.map(largeData, function(x){ return x.value * 2 })
* def finalMemory = memory()
* print 'Final memory:', finalMemory

Advanced Java Integration

Thread-Safe Java Functions

When running tests in parallel, multiple threads may call your Java utility methods simultaneously. Standard Java variables are not thread-safe and can cause race conditions. Use AtomicInteger for counters and ConcurrentHashMap for shared caches to ensure correct behavior:

Java
package com.example.karate;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class TestUtils {
// Thread-safe counter
private static final AtomicInteger counter = new AtomicInteger(0);

// Thread-safe cache
private static final Map<String, Object> cache =
new ConcurrentHashMap<>();

public static int getNextId() {
return counter.incrementAndGet();
}

public static void cacheValue(String key, Object value) {
cache.put(key, value);
}

public static Object getCachedValue(String key) {
return cache.get(key);
}
}

Use in tests:

Gherkin
Feature: Thread-safe utilities

Background:
* def Utils = Java.type('com.example.karate.TestUtils')

Scenario: Thread-safe operations
* def id1 = Utils.getNextId()
* def id2 = Utils.getNextId()
* assert id2 == id1 + 1
* Utils.cacheValue('token', 'abc123')
* def token = Utils.getCachedValue('token')
* match token == 'abc123'

Custom Hooks

Hooks let you execute custom code at specific points in the test lifecycle - before/after each scenario, feature, or the entire test run. Implement the RuntimeHook interface and register it with the Runner. Common use cases include:

  • Timing and metrics - Measure scenario duration
  • Custom logging - Log scenario names and results
  • Resource cleanup - Close connections after tests
  • Dynamic variables - Set variables based on external state
Java
package com.example.karate;

import com.intuit.karate.RuntimeHook;
import com.intuit.karate.core.ScenarioRuntime;

public class CustomHook implements RuntimeHook {

@Override
public boolean beforeScenario(ScenarioRuntime sr) {
System.out.println("Starting: " + sr.scenario.getName());
sr.engine.setVariable("startTime",
System.currentTimeMillis());
return true;
}

@Override
public void afterScenario(ScenarioRuntime sr) {
long start = (Long) sr.engine.getVariable("startTime");
long duration = System.currentTimeMillis() - start;
System.out.println("Completed in: " + duration + " ms");
}
}

Use the hook:

Java
Results results = Runner.path("classpath:features")
.hook(new CustomHook())
.parallel(5);

Next Steps

Master Java integration and continue with: