ADVANCED
Polling and Async Operations
Test asynchronous operations, eventual consistency, and event-driven systems using retry patterns, WebSocket connections, and custom polling logic.
On this page:
- retry until - Built-in retry mechanism for polling
- configure retry - Set retry count and interval
- listen / signal - Handle async events and callbacks
- karate.webSocket() - WebSocket connections and messaging
Retry Until Pattern
Basic Retry
Use retry until to wait for a condition to become true. The retry until keyword must appear before the method step:
Feature: Basic retry
Scenario: Wait for data to appear
* configure retry = { count: 10, interval: 2000 }
Given url 'https://jsonplaceholder.typicode.com'
And path 'users', 1
And retry until response.id == 1
When method get
Then status 200
Retry Configuration
Configure retry attempts and interval globally or per-scenario:
Feature: Retry configuration
Scenario: Custom retry settings
# Set retry: 5 attempts, 2 second intervals
* configure retry = { count: 5, interval: 2000 }
Given url 'https://jsonplaceholder.typicode.com'
And path 'posts', 1
And retry until responseStatus == 200
When method get
Default is 3 attempts with 3000ms (3 second) intervals. Configure via configure retry = { count: 3, interval: 3000 }.
Custom Retry Conditions
Use JavaScript expressions in retry conditions. You can reference response, responseStatus, or responseHeaders:
Feature: Complex retry conditions
Scenario: Retry with status check
* configure retry = { count: 5, interval: 1000 }
Given url 'https://jsonplaceholder.typicode.com'
And path 'users', 1
And retry until responseStatus == 200 && response.id > 0
When method get
Then status 200
* match response.id == 1
The retry until expression must be pure JavaScript. Karate match syntax like contains will NOT work. Use JavaScript equivalents instead.
Custom Polling Logic
JavaScript Polling Functions
For complex polling scenarios beyond retry until, use karate.http() in JavaScript functions:
Feature: Custom polling
Background:
* def pollUntilReady =
"""
function(url, maxAttempts, interval) {
for (var i = 0; i < maxAttempts; i++) {
var response = karate.http(url).get();
if (response.status == 200) {
return response.body;
}
java.lang.Thread.sleep(interval);
}
karate.fail('Polling timeout after ' + maxAttempts + ' attempts');
}
"""
Scenario: Use custom polling
* def result = pollUntilReady('https://jsonplaceholder.typicode.com/users/1', 5, 1000)
* match result.id == 1
karate.http(url) returns a convenience HTTP request builder for use within JavaScript. Use .get(), .post(), .put(), or .delete() to execute requests.
Progressive Backoff
Exponential backoff doubles the delay between each retry attempt, reducing load on the server while waiting for eventual consistency:
Feature: Exponential backoff
Background:
* def pollWithBackoff =
"""
function(checkFunc, maxAttempts) {
var delay = 1000;
for (var i = 0; i < maxAttempts; i++) {
var result = checkFunc();
if (result.success) return result.data;
java.lang.Thread.sleep(delay);
delay = delay * 2;
}
karate.fail('Max attempts reached');
}
"""
* def checkStatus =
"""
function() {
var res = karate.http('https://jsonplaceholder.typicode.com/users/1').get();
return { success: res.status == 200, data: res.body };
}
"""
Scenario: Poll with backoff
* def result = pollWithBackoff(checkStatus, 5)
* match result.id == 1
Async Event Handling
Listen and Signal
The listen keyword waits for an event, and karate.signal(data) triggers that event. This pattern is useful for message queues, WebSockets, and custom async callbacks.
Feature: Async event handling
Background:
* def QueueConsumer = Java.type('mock.contract.QueueConsumer')
* def queue = new QueueConsumer(queueName)
* def handler = function(msg) { karate.signal(msg) }
* queue.listen(karate.toJava(handler))
Scenario: Wait for async event
Given url paymentServiceUrl + '/payments'
And request { amount: 5.67, description: 'test one' }
When method post
Then status 200
* def id = response.id
# Wait for message on queue (max 5 seconds)
* listen 5000
* json shipment = listenResult
* match shipment == { paymentId: '#(id)', status: 'shipped' }
listen accepts timeout in milliseconds. If no event arrives, listenResult will be null. Always check for null to handle timeouts gracefully.
Signal API Reference
The karate.signal(data) method and listenResult variable work together:
| Component | Description |
|---|---|
karate.signal(data) | Trigger event with any data type (JSON, string, number, array) |
listenResult | Magic variable containing the signaled data |
karate.toJava(fn) | Wrap JS functions for use in Java callbacks |
Handling timeouts:
Feature: Timeout handling
Scenario: Handle listen timeout
* listen 5000
* if (listenResult == null) karate.fail('Timeout waiting for event')
Multiple Concurrent Listeners
When testing workflows that produce multiple events, collect them in an array and signal when all expected events have arrived:
Feature: Multiple async sources
Background:
* def events = []
* def eventHandler = function(event) {
events.push(event);
if (events.length >= 2) {
karate.signal(events);
}
}
Scenario: Wait for multiple events
* def EventBus = Java.type('com.example.EventBus')
* EventBus.subscribe('channel-1', karate.toJava(eventHandler))
* EventBus.subscribe('channel-2', karate.toJava(eventHandler))
* karate.call('trigger-events.feature')
# Wait for both events
* listen 15000
* def allEvents = listenResult
* match allEvents == '#[2]'
Message Queue Integration
Test message queue interactions using the listen/signal pattern:
Feature: Message queue testing
Background:
* def QueueConsumer = Java.type('mock.contract.QueueConsumer')
* def queue = new QueueConsumer('order-queue')
* def messageHandler = function(msg) {
if (msg.type === 'order-processed') {
karate.signal(msg);
}
}
* queue.listen(karate.toJava(messageHandler))
Scenario: Test queue message flow
Given url orderServiceUrl + '/orders'
And request { orderId: 'ORD-123', items: ['item1'] }
When method post
Then status 201
# Wait for message on queue
* listen 5000
* def message = listenResult
* match message.orderId == 'ORD-123'
* match message.type == 'order-processed'
The listen/signal pattern works with any message broker (ActiveMQ, RabbitMQ, Kafka) via custom Java classes that forward messages to karate.signal().
WebSocket Support
Karate has built-in WebSocket support using the async listen pattern.
Basic WebSocket Connection
Connect to a WebSocket endpoint, send a message, and wait for a response using listen:
Feature: WebSocket testing
Scenario: Basic WebSocket communication
* def socket = karate.webSocket(demoBaseUrl + '/websocket')
* socket.send('Billie')
* listen 5000
* match listenResult == 'hello Billie !'
WebSocket examples use demoBaseUrl as a placeholder. Configure your actual WebSocket server URL in karate-config.js.
WebSocket with Handler
A handler function filters messages—return true to complete the listen wait:
Feature: WebSocket with filtering
Scenario: Filter WebSocket messages
* def handler = function(msg) { return msg.startsWith('hello') }
* def socket = karate.webSocket(demoBaseUrl + '/websocket', handler)
* socket.send('Billie')
* listen 5000
* match listenResult == 'hello Billie !'
Without a handler (or null), the first message received completes the listen.
WebSocket Options
The third argument to karate.webSocket() accepts configuration options:
Feature: WebSocket options
Scenario: WebSocket with options
* def wsOptions = { subProtocol: 'chat', headers: { 'Authorization': 'Bearer token' }, maxPayloadSize: 8388608 }
* def ws = karate.webSocket(demoBaseUrl + '/websocket', null, wsOptions)
* ws.send('hello')
* listen 5000
* match listenResult == '#notnull'
| Option | Description |
|---|---|
subProtocol | WebSocket sub-protocol if server requires it |
headers | Custom headers (e.g., authentication) |
maxPayloadSize | Max message size in bytes (default: ~4MB) |
WebSocket connections are auto-closed at the end of each Scenario. Use ws.close() for explicit cleanup.
Binary WebSocket Messages
For binary protocols (custom wire formats, file transfer, compressed data), use karate.webSocketBinary() instead of the text-based version:
Feature: Binary WebSocket
Scenario: Binary WebSocket communication
* def ws = karate.webSocketBinary(demoBaseUrl + '/binary')
* def binaryData = read('classpath:test-data.bin')
* ws.send(binaryData)
* listen 5000
* match listenResult == '#notnull'
* assert listenResult.length > 0
WebSocket Connection Management
Send multiple messages on the same connection:
Feature: WebSocket lifecycle
Scenario: Multiple messages
* def ws = karate.webSocket(demoBaseUrl + '/websocket')
* ws.send('first')
* listen 2000
* ws.send('second')
* listen 2000
* ws.close()
Reconnection with retry - Handle flaky WebSocket servers by wrapping connection logic in a retry loop:
Feature: WebSocket reconnection
Background:
* def connectWithRetry =
"""
function(url, maxRetries) {
for (var i = 0; i < maxRetries; i++) {
try {
return karate.webSocket(url);
} catch (e) {
karate.log('Connection failed, attempt:', i + 1);
if (i < maxRetries - 1) java.lang.Thread.sleep(2000);
else throw e;
}
}
}
"""
Scenario: Connect with retry
* def ws = connectWithRetry(demoBaseUrl + '/websocket', 3)
* ws.send('hello')
* listen 5000
* match listenResult == '#notnull'
Advanced Patterns
Event Correlation
Filter events by correlation ID to track related async operations:
Feature: Event correlation
Background:
* def correlationId = java.util.UUID.randomUUID().toString()
* def events = []
* def eventHandler = function(event) {
if (event.correlationId === correlationId) {
events.push(event);
if (events.length >= 3) {
karate.signal(events);
}
}
}
Scenario: Correlate multiple events
* def EventSystem = Java.type('com.example.EventSystem')
* EventSystem.registerHandler(karate.toJava(eventHandler))
Given url workflowServiceUrl + '/async-workflow'
And request { correlationId: '#(correlationId)', steps: ['validate', 'process', 'notify'] }
When method post
Then status 202
# Wait for all 3 correlated events
* listen 15000
* def allEvents = listenResult
* match allEvents == '#[3]'
* match each allEvents[*].correlationId == correlationId
Timeout Strategies
For unreliable services, try with short timeouts first and progressively increase if needed:
Feature: Cascading timeouts
Background:
* def tryWithTimeouts =
"""
function(operation, timeouts) {
for (var i = 0; i < timeouts.length; i++) {
try {
karate.configure('readTimeout', timeouts[i]);
return operation();
} catch (e) {
if (i === timeouts.length - 1) throw e;
}
}
}
"""
Scenario: Cascading timeout strategy
* def callApi = function() {
return karate.http('https://jsonplaceholder.typicode.com/users/1').get();
}
# Try with increasing timeouts: 1s, 5s, 10s
* def result = tryWithTimeouts(callApi, [1000, 5000, 10000])
* match result.status == 200
Combining Polling and Events
Combine HTTP triggers with WebSocket confirmation for complete async workflows:
Feature: Complete async workflow
Background:
* def notificationHandler = function(msg) {
var data = JSON.parse(msg);
return data.type === 'job-completed';
}
* def ws = karate.webSocket(demoBaseUrl + '/notifications', notificationHandler)
Scenario: API trigger with event confirmation
# Trigger job via API
Given url jobServiceUrl + '/jobs'
And request { operation: 'process-data', priority: 'high' }
When method post
Then status 202
* def jobId = response.jobId
# Subscribe and wait for completion
* ws.send({ action: 'subscribe', jobId: jobId })
* listen 30000
* def notification = JSON.parse(listenResult)
* match notification.status == 'completed'
# Verify final state
Given path 'jobs', jobId
When method get
Then status 200
* match response.result == '#present'
This pattern combines HTTP (reliable, stateless) with WebSocket (real-time) for robust async testing.
When to Use Polling and Async
Use retry mechanisms when:
- Testing services with startup delays or eventual consistency
- Working with distributed systems that have propagation delays
- Dealing with rate-limited or throttled APIs that require backoff
Use async event handling when:
- Testing WebSocket real-time communication
- Integrating with message queues or event buses
- Coordinating multiple asynchronous operations across services
Configure appropriate timeouts based on:
- Development environment: Longer timeouts (30-60 seconds)
- CI/CD pipelines: Moderate timeouts (10-20 seconds)
- Production testing: Strict timeouts matching SLAs
Next Steps
- Add conditional logic: Conditional Logic
- Manage test lifecycle: Hooks
- Debug async issues: Debugging
- Run tests in parallel: Parallel Execution