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 — the karate CLI

To run tests without Maven, Gradle, or any Java setup, install the Karate CLI. The Rust launcher bootstraps the JRE and the Karate fatjar for you — zero manual Java install, zero build tool.

See Standalone Execution for installation, and Command Line for the full CLI reference. For the scenarios below, the entry point is simply karate run.

Java DSL (embedding Karate in your code)

When you need Java code instead of .feature files — one-off scripts, quick API checks, or embedding Karate inside an existing Java app — use the Java DSL. The Http, Json, and Match classes work standalone without a test runner:

Java
import io.karatelabs.http.Http;
import io.karatelabs.common.Json;
import io.karatelabs.match.Match;
import java.util.List;

public class Example {
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());
}
}

Add io.karatelabs:karate-core to your Maven or Gradle build to use these classes.

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
  • SuiteResult - Object containing pass/fail counts and timing data
Java
import io.karatelabs.core.Runner;
import io.karatelabs.core.SuiteResult;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

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

assertFalse(results.isFailed());
}

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() {
SuiteResult results = Runner.path("classpath:api", "classpath:smoke.feature")
.tags("@regression", "~@wip")
.karateEnv("staging")
.outputCucumberJson(true)
.outputJunitXml(true)
.outputDir("target/karate-reports")
.parallel(10);

assertFalse(results.isFailed(),
"Found " + results.getScenarioFailedCount() + " 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 — returns SuiteResult)
outputDir(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)
outputJsonLines(boolean)Emit JSONL event stream
dryRun(boolean)Skip step execution and hooks while still generating a full report
listener(RunListener)Add a run event listener (replaces v1 hook())

Dry Run Mode

Produce a full report without executing any steps — useful for feature-file validation, tag-coverage review, and CI smoke passes that don't need real I/O:

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

Under dry run, every step on a normal scenario is recorded as passed with 0ms duration. Config JS (karate-base.js, karate-config.js) and beforeScenario / afterScenario hooks are skipped. @setup scenarios still execute fully so dynamic scenario outlines resolve their rows. Inside an @setup scenario, read karate.suite.dryRun to branch on mode — e.g. return placeholder rows instead of hitting a real database.

See Dry Run for the full behavior matrix.

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 UI test, or to verify database state after an operation. Runner.runFeature() executes a single feature and returns a FeatureResult whose getResultVariables() gives you response, custom variables, or any other data set during execution.

Java
import io.karatelabs.core.Runner;
import io.karatelabs.core.FeatureResult;
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 the feature
FeatureResult result = Runner.runFeature(
"classpath:features/get-user.feature",
vars
);

// Access variables set in the feature
Map<String, Object> out = result.getResultVariables();
Object user = out.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="io.karatelabs" level="DEBUG"/>

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

Logger Categories

Karate v2 emits logs under named SLF4J categories so you can tune verbosity per concern instead of flipping the whole engine to DEBUG:

CategoryWhat it covers
karate.runtimeFeature/scenario lifecycle, step execution, suite orchestration
karate.httpHTTP client — request/response bodies, headers, retries
karate.mockMock server — incoming requests, matched scenarios, responses
karate.scenarioUser output — print, karate.log(), scenario-scoped messages
karate.consoleCLI console output — summary, colors, progress

Enable selectively — for example, DEBUG only for HTTP traffic while keeping the rest at INFO:

logback-test.xml
<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="karate.http" level="DEBUG"/>
<logger name="karate.mock" level="DEBUG"/>

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

Skip logback entirely with karate run --log-console debug. For reports (independent of console output), use --log-report <level> or configure logging = { report: 'warn' } inside a feature. See Logging.

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="io.karatelabs" level="INFO"/>
</then>
<else>
<logger name="io.karatelabs" 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="io.karatelabs" level="DEBUG"/>

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

Logging and Report Verbosity

Karate v2 unifies log behavior under a single configure logging bucket — masking sensitive data, controlling verbosity, and toggling pretty-printing. The full reference is in Logging; below are the patterns most relevant when integrating with a Java codebase.

Mask sensitive data declaratively

When testing auth endpoints, request/response bodies contain passwords, tokens, and API keys. Mask them at the source so they never reach logs or reports. The shape is declarative — no Java class needed:

Gherkin
Background:
* configure logging = {
mask: {
headers: ['Authorization', 'Cookie', 'X-Api-Key'],
jsonPaths: ['$.password', '$..token'],
patterns: [{ regex: 'Bearer [A-Za-z0-9._-]+', replacement: 'Bearer ***' }],
enableForUri: function(uri){ return uri.indexOf('/auth') >= 0 }
}
}

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
# Authorization header, password, and bearer tokens are all masked in logs and reports

The same shape works in karate-config.js to apply across the whole suite.

Hide a scenario from reports

Tag a scenario @report=false to keep it in the run + suite totals but suppress its step detail from HTML / Cucumber JSON / JUnit XML / JSONL outputs. Failures surface only a redacted message; full detail still hits SLF4J for local debugging:

Gherkin
@report=false
Scenario: warmup with sensitive credentials
* call read('classpath:auth/login.feature')

Quiet a scenario by raising the threshold

To drop INFO-level captured output (HTTP body logs, print, karate.log) without hiding the scenario row:

Gherkin
* configure logging = { report: 'warn' }

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') {
// Quiet CI — drop INFO captures (HTTP bodies, print, karate.log)
// and redact secrets that may slip into HTTP logs.
karate.configure('logging', {
report: 'warn',
console: 'warn',
mask: {
headers: ['Authorization', 'Cookie', 'X-Api-Key'],
jsonPaths: ['$.password', '$..token']
}
});

// 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 SuiteResult returned by Runner.parallel() contains execution statistics. Use these to monitor test health, identify slow tests, or generate custom CI reports:

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

System.out.println("Scenarios passed: " + results.getScenarioPassedCount());
System.out.println("Scenarios failed: " + results.getScenarioFailedCount());
System.out.println("Total time: " + results.getDurationMillis() + "ms");
System.out.println("Features: " + results.getFeaturePassedCount() + "/" +
results.getFeatureCount());

if (results.isFailed()) {
System.err.println("Suite failed — see " + results.getReportDir());
}

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

Passing JS Functions to Java Methods

JS functions auto-coerce to single-method Java functional interfaces. Pass an inline arrow / function directly to any Java method that declares Function, Predicate, Consumer, Supplier, or Runnable — no wrapper, no karate.toJava() (which is a deprecated no-op in v2):

DbUtils.java
public List<Map<String, Object>> waitForRows(String sql, int expected,
int timeoutMs,
Predicate<Map<String, Object>> condition) {
// ... polling implementation ...
}
Gherkin
Background:
* def DbUtils = Java.type('com.mycompany.DbUtils')

Scenario: Wait for a row matching a JS predicate
* def isShipped = row => row.status === 'SHIPPED'
* def rows = DbUtils.waitForRows('SELECT * FROM orders WHERE id = 42', 1, 5000, isShipped)
* match rows[0].status == 'SHIPPED'

Notes:

  • Predicate.test() uses JS-truthy semantics — return any value, not just a boolean. A non-empty string, non-zero number, or object is treated as true; undefined, null, 0, and '' are false.
  • Function.apply() and Supplier.get() auto-unwrap return values for Java consumers (undefined → null, JS Date → java.util.Date).
  • For multi-arg interfaces (BiFunction, BiConsumer, etc.), receive the JS function as JavaCallable and call .call(null, arg1, arg2) explicitly.

When to Use What

Choose the right tool based on your testing needs:

NeedUse ThisWhy
Run tests without build toolKarate CLINo Maven, Gradle, or manual Java install
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 Listeners

Listeners let you observe and control the test lifecycle — before/after each scenario, feature, HTTP call, or the entire suite. In Karate v2, the old RuntimeHook and ResultListener are unified into a single RunListener interface with one method: onEvent(RunEvent). Common use cases:

  • Timing and metrics — measure scenario duration
  • Custom logging — log scenario names and results
  • Resource cleanup — close connections after tests
  • Skip/mock HTTP calls — return false from HTTP_ENTER to bypass real requests
Java
package com.example.karate;

import io.karatelabs.core.RunListener;
import io.karatelabs.core.RunEvent;
import io.karatelabs.core.RunEventType;
import io.karatelabs.core.ScenarioRuntime;

public class TimingListener implements RunListener {

@Override
public boolean onEvent(RunEvent event) {
if (event.type() == RunEventType.SCENARIO_ENTER) {
ScenarioRuntime sr = event.scenarioRuntime();
System.out.println("Starting: " + sr.scenario.getName());
sr.engine.setVariable("startTime", System.currentTimeMillis());
} else if (event.type() == RunEventType.SCENARIO_EXIT) {
ScenarioRuntime sr = event.scenarioRuntime();
long start = (Long) sr.engine.getVariable("startTime");
System.out.println("Completed in: " + (System.currentTimeMillis() - start) + " ms");
}
return true; // return false from *_ENTER events to skip execution
}
}

Register the listener:

Java
SuiteResult results = Runner.path("classpath:features")
.listener(new TimingListener())
.parallel(5);

Next Steps

Master Java integration and continue with: