ADVANCED
Polling and Async Operations
Test asynchronous operations, eventual consistency, and event-driven systems using retry patterns, WebSocket connections, and custom polling logic.
Why Polling and Async?
- Eventual consistency: Test systems where data becomes consistent over time, not immediately
- Event-driven architectures: Handle WebSockets, message queues, and async callbacks seamlessly
- Resilient testing: Retry mechanisms handle flaky services and startup delays automatically
Retry Until Pattern
Basic Retry
Use retry until
to wait for a condition to become true:
Feature: Basic retry
Scenario: Wait for service to be ready
* configure retry = { count: 10, interval: 2000 }
* retry until responseStatus == 200
Given url serviceUrl + '/health'
When method get
Scenario: Wait for job completion
* retry until response.status == 'completed'
Given url apiUrl + '/job/123'
When method get
Then match response.result == '#present'
Configure retry globally or per-scenario: configure retry = { count: 5, interval: 2000 }
. Default is 3 attempts with 3-second intervals.
Custom Retry Conditions
Use complex expressions in retry conditions:
Feature: Complex retry conditions
Scenario: Retry with multiple conditions
* def maxAttempts = 10
* def attempts = 0
* retry until response.status == 'success' || attempts++ >= maxAttempts
Given url apiUrl + '/async-process'
When method get
Then status 200
Scenario: Retry until data appears
* retry until response.items.length > 0
Given url apiUrl + '/search'
And param q = 'test'
When method get
Then match response.items == '#[_ > 0]'
Custom Polling Logic
JavaScript Polling Functions
Create reusable polling functions for complex scenarios:
Feature: Custom polling
Background:
* def pollUntilReady =
"""
function(url, maxAttempts, interval) {
for (var i = 0; i < maxAttempts; i++) {
var result = karate.call('check-status.feature', { checkUrl: url });
if (result.response.status === 'ready') {
return result.response;
}
java.lang.Thread.sleep(interval);
}
karate.fail('Polling timeout after ' + maxAttempts + ' attempts');
}
"""
Scenario: Use custom polling
* def result = pollUntilReady(apiUrl + '/job/123', 10, 2000)
* match result.status == 'ready'
* match result.data == '#present'
Progressive Backoff
Implement exponential backoff for efficient polling:
Feature: Exponential backoff
Background:
* def pollWithBackoff =
"""
function(checkFunc, maxAttempts) {
var delay = 1000; # Start with 1 second
for (var i = 0; i < maxAttempts; i++) {
var result = checkFunc();
if (result.success) return result.data;
java.lang.Thread.sleep(delay);
delay = delay * 2; # Double the delay each time
}
karate.fail('Max attempts reached');
}
"""
Scenario: Poll with backoff
* def checkStatus = function() {
var res = karate.http({ url: apiUrl + '/status' }).get();
return { success: res.status === 200 && res.body.ready, data: res.body };
}
* def result = pollWithBackoff(checkStatus, 5)
* match result.ready == true
Async Event Handling
Listen and Signal
Handle asynchronous events with listen
and signal
:
Feature: Async event handling
Background:
# Assuming you have a custom EventHandler class
* def EventHandler = Java.type('com.mycompany.EventHandler')
* def handler = new EventHandler()
* def onEvent = function(event) { karate.signal(event) }
* handler.registerCallback(karate.toJava(onEvent))
Scenario: Wait for async event
# Trigger async operation
Given url apiUrl + '/trigger-async'
And request { operation: 'process', id: 123 }
When method post
Then status 202
# Wait for event (max 10 seconds)
* listen 10000
* def event = listenResult
* match event == { type: 'completed', id: 123, status: 'success' }
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 to pass data from async callbacks to your test:
Feature: Signal API demonstration
Background:
* def asyncCallback = function(data) {
karate.log('Received async data:', data);
karate.signal({ timestamp: new Date().getTime(), data: data });
}
Scenario: Using signal API
# Register callback (implementation depends on your system)
* def AsyncProcessor = Java.type('com.example.AsyncProcessor')
* AsyncProcessor.register(karate.toJava(asyncCallback))
# Trigger async operation
Given url apiUrl + '/trigger'
When method post
Then status 202
# Wait for signal
* listen 10000
* print 'Received:', listenResult
* match listenResult.data == '#present'
* match listenResult.timestamp == '#number'
Key points:
karate.signal(data)
- Pass any data type (JSON, string, number, array)listenResult
- Magic variable containing signaled data- Timeout returns
null
if no signal received - Signal can be called from Java callbacks via
karate.toJava()
Handling timeouts:
* listen 5000
* if (listenResult == null) karate.fail('Timeout waiting for event')
* match listenResult.status == 'completed'
Multiple Concurrent Listeners
Handle multiple async sources with different timeouts:
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
# Register for multiple event sources
* def EventBus = Java.type('com.example.EventBus')
* EventBus.subscribe('channel-1', karate.toJava(eventHandler))
* EventBus.subscribe('channel-2', karate.toJava(eventHandler))
# Trigger operations that generate events
* call read('trigger-events.feature')
# Wait for both events to arrive
* listen 15000
* def allEvents = listenResult
* match allEvents == '#[2]'
* match allEvents[*].source contains ['channel-1', 'channel-2']
Error recovery for async failures:
Scenario: Async error handling
* def errorHandler = function(error) {
karate.signal({ error: true, message: error.toString() });
}
* listen 10000
* if (listenResult.error) karate.log('Async operation failed:', listenResult.message)
* match listenResult.error == false
Message Queue Integration
Test message queue interactions using the listen/signal pattern:
Feature: Message queue testing
Background:
# Assuming you have a custom QueueConsumer class
* def QueueConsumer = Java.type('com.mycompany.QueueConsumer')
* def queue = new QueueConsumer('test-queue')
* def messageHandler = function(msg) {
if (msg.type === 'order-processed') {
karate.signal(msg);
}
}
* queue.listen(karate.toJava(messageHandler))
Scenario: Test queue message flow
# Send message that triggers queue activity
Given url apiUrl + '/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'
For comprehensive message queue integration including ActiveMQ, RabbitMQ, and JMS patterns, see Message Queue Integration.
WebSocket Support
Basic WebSocket Connection
Connect to WebSocket endpoints:
Feature: WebSocket testing
Scenario: Basic WebSocket communication
# Create WebSocket connection
* def ws = karate.webSocket('ws://localhost:8080/socket')
# Send and receive text
* ws.send('Hello Server')
* listen 5000
* match listenResult == 'Hello Client'
# Send and receive JSON
* ws.send({ type: 'ping', timestamp: '#(new Date().getTime())' })
* listen 5000
* match listenResult == { type: 'pong', timestamp: '#number' }
WebSocket with Handler
Filter WebSocket messages with custom handlers:
Feature: WebSocket with filtering
Scenario: Filter WebSocket messages
# Handler that only signals for notifications
* def handler = function(msg) {
var data = JSON.parse(msg);
return data.type === 'notification';
}
# Create WebSocket with handler
* def ws = karate.webSocket('ws://localhost:8080/events', handler)
# Send subscription request
* ws.send({ action: 'subscribe', channel: 'updates' })
# Wait for notification (ignores other messages)
* listen 10000
* def notification = JSON.parse(listenResult)
* match notification.type == 'notification'
* match notification.channel == 'updates'
WebSocket Options
Configure WebSocket connections with authentication and options:
Feature: Secure WebSocket
Scenario: WebSocket with authentication
* def token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
* def wsOptions = {
subProtocol: 'chat',
headers: { 'Authorization': 'Bearer ' + token },
maxPayloadSize: 8388608
}
* def ws = karate.webSocket('wss://api.example.com/socket', null, wsOptions)
* ws.send({ action: 'getData' })
* listen 5000
* match listenResult contains { data: '#present' }
Always close WebSocket connections when done to prevent resource leaks. Use ws.close()
or wrap in try-finally blocks.
Binary WebSocket Messages
Use karate.webSocketBinary()
for binary protocols:
Feature: Binary WebSocket
Scenario: Binary WebSocket communication
* def ws = karate.webSocketBinary('ws://localhost:8080/binary')
# Send binary data
* def binaryData = read('test-data.bin')
* ws.send(binaryData)
# Receive binary response
* listen 5000
* def response = listenResult
* match response == '#notnull'
* assert response.length > 0
Use cases for binary WebSockets:
- Custom binary protocols
- File transfer over WebSocket
- Real-time audio/video streaming data
- Compressed data transmission
WebSocket Connection Management
Manage WebSocket lifecycle explicitly:
Feature: WebSocket lifecycle
Scenario: Explicit connection management
* def ws = karate.webSocket('ws://localhost:8080/stream')
# Send multiple messages
* ws.send({ action: 'subscribe', topic: 'updates' })
* listen 2000
* match listenResult.subscribed == true
* ws.send({ action: 'fetch', id: 123 })
* listen 5000
* match listenResult.data == '#present'
# Close connection explicitly
* ws.close()
Reconnection strategy:
Feature: WebSocket reconnection
Background:
* def connectWebSocket =
"""
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 = connectWebSocket('ws://flaky-server.com/socket', 3)
* ws.send('hello')
* listen 5000
* match listenResult == '#notnull'
Advanced Patterns
Event Correlation
Correlate multiple related async events:
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) { # Expecting 3 events
karate.signal(events);
}
}
}
Scenario: Correlate multiple events
# Assuming you have event system configured
* def EventSystem = Java.type('com.mycompany.EventSystem')
* EventSystem.registerHandler(karate.toJava(eventHandler))
# Start async process
Given url apiUrl + '/async-workflow'
And request { correlationId: '#(correlationId)', steps: ['validate', 'process', 'notify'] }
When method post
Then status 202
# Wait for all correlated events
* listen 15000
* def allEvents = listenResult
* match allEvents == '#[3]'
* match each allEvents[*].correlationId == correlationId
Timeout Strategies
Implement cascading timeout strategies:
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({ url: apiUrl + '/slow-endpoint' }).get();
}
# Try with increasing timeouts: 1s, 5s, 10s
* def result = tryWithTimeouts(callApi, [1000, 5000, 10000])
* match result.status == 200
Combining Polling and Events
Trigger operations via HTTP and confirm completion via WebSocket:
Feature: Complete async workflow
Background:
# Set up WebSocket for notifications
* def notificationHandler = function(msg) {
var data = JSON.parse(msg);
return data.type === 'job-completed';
}
* def ws = karate.webSocket('ws://localhost:8080/notifications', notificationHandler)
Scenario: API trigger with event confirmation
# Trigger long-running job via API
Given url apiUrl + '/jobs'
And request { operation: 'process-data', priority: 'high' }
When method post
Then status 202
* def jobId = response.jobId
# Subscribe to job notifications via WebSocket
* ws.send({ action: 'subscribe', jobId: '#(jobId)' })
# Wait for completion notification (up to 30 seconds)
* listen 30000
* def notification = JSON.parse(listenResult)
* match notification.jobId == jobId
* match notification.status == 'completed'
# Verify final state via API
Given path 'jobs', jobId
When method get
Then status 200
And match response.result == '#present'
And match response.completedAt == '#string'
This pattern combines the strengths of HTTP (reliable, stateless) with WebSocket (real-time, efficient) 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