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:
- jbang - Run tests without a build tool
- Runner API - Programmatic test execution
- Runner.runFeature() - Call features from Java code
- karate.callSingle() - Run code once across parallel tests
- Logging - Logback configuration
- Log masking - Hide sensitive data
- Report control - Configure report output
- CI/CD - Pipeline-friendly configuration
- Utilities - Dynamic ports, reusable functions
- Java integration - Thread-safe utilities and hooks
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.
# 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:
///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());
}
}
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 runResults- Object containing pass/fail counts, error messages, and timing data
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:
@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
| Method | Description |
|---|---|
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:
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.
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);
}
}
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
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:
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:
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 };
}
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:
<?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:
<?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:
<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()- Returntruefor 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)
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:
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
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)
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:
@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:
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:
- Disables print statements to reduce noise
- Minimizes report verbosity
- Captures only failure information for debugging
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:
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:
function fn() {
var port = karate.properties['server.port'] || '8080';
var config = {
baseUrl: 'http://localhost:' + port + '/api'
};
return config;
}
Run tests with dynamic port:
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():
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:
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:
- Load the Java class with
Java.type('fully.qualified.ClassName') - Call static methods directly on the type, or use
newfor instance methods
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:
| Need | Use This | Why |
|---|---|---|
| Run tests without build tool | jbang | Zero setup, instant execution |
| Programmatic test control | Runner API | Full Java integration |
| Mask passwords/tokens | HttpLogModifier | Security compliance |
| CI/CD logging | Environment config | Reduce pipeline noise |
| Performance tracking | Custom functions | Monitor bottlenecks |
| Reusable utilities | utils.js | DRY principle |
| Dynamic configuration | karate-config.js | Environment switching |
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:
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:
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:
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:
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
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:
Results results = Runner.path("classpath:features")
.hook(new CustomHook())
.parallel(5);
Next Steps
Master Java integration and continue with:
- Debug tests effectively: Debugging
- Configure test lifecycle: Hooks
- Optimize parallel execution: Parallel Execution
- Review test reports: Test Reports
- Explore Karate object API: Karate Object