Skip to main content

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'
Retry Configuration

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 Timeout

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'
Dedicated Message Queue Guide

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' }
WebSocket Cleanup

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