Skip to main content

ADVANCED

Polling and Async Operations

What You'll Learn

  • Implement retry mechanisms for eventually consistent systems
  • Use listen and signal for asynchronous event handling
  • Connect and test WebSocket endpoints
  • Create custom polling logic with JavaScript functions
  • Handle message queues and event-driven architectures
  • Configure timeouts and retry intervals for reliability

Overview

Modern systems often involve asynchronous operations, eventual consistency, and event-driven architectures. Karate provides powerful mechanisms for testing these scenarios through retry patterns, polling logic, WebSocket support, and async event handling.

Retry Until Pattern

Basic Retry

The retry until syntax allows you to retry operations until a condition is met:

Feature: Retry mechanism

Scenario: Wait for service to be ready
* retry until responseStatus == 200
Given url serviceUrl + '/health'
When method get

Scenario: Wait for specific response
* retry until response.status == 'completed'
Given url apiUrl + '/job/123'
When method get
Then match response.result == '#present'
```gherkin

### Configuring Retry

Configure retry behavior with count and interval:

```gherkin
# Global configuration
* configure retry = { count: 10, interval: 2000 }

# Per-scenario configuration
Scenario: Custom retry settings
* configure retry = { count: 5, interval: 1000 }
* retry until response.ready == true
Given url apiUrl + '/status'
When method get
```gherkin

### Complex Retry Conditions

Use complex expressions in retry conditions:

```gherkin
Scenario: Retry with complex conditions
# Retry until status is success or max attempts reached
* def attempt = 0
* retry until response.status == 'success' || attempt++ > 10
Given url apiUrl + '/process'
When method get

# Validate final state
* if (response.status != 'success') karate.fail('Process did not complete')
```gherkin

## Custom Polling Logic

### JavaScript Polling Functions

Create custom polling logic with JavaScript:

```gherkin
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'
```gherkin

### Progressive Backoff

Implement exponential backoff for polling:

```gherkin
* 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;
}
karate.log('Attempt', i + 1, 'failed, waiting', delay, 'ms');
java.lang.Thread.sleep(delay);
delay = delay * 2; // Double the delay each time
}
karate.fail('Max attempts reached');
}
"""

* def checkStatus = function() {
var response = karate.http({ url: apiUrl + '/status' }).get();
return { success: response.status === 200, data: response.body };
}

* def result = pollWithBackoff(checkStatus, 5)
```gherkin

## Async Event Handling

### Listen and Signal

Use `listen` and `karate.signal()` for async event handling:

```gherkin
Feature: Async event handling

Background:
# Set up event handler
* def EventHandler = Java.type('com.example.EventHandler')
* def handler = new EventHandler()

# Define callback function
* def onEvent = function(event) { karate.signal(event) }
* handler.registerCallback(karate.toJava(onEvent))

Scenario: Wait for async event
# Trigger async operation
* url apiUrl + '/trigger-async'
* request { operation: 'process', id: 123 }
* method post
* status 202

# Wait for event (max 10 seconds)
* listen 10000

# Validate received event
* def event = listenResult
* match event == { type: 'completed', id: 123, status: 'success' }
```gherkin

### Message Queue Integration

Test message queue interactions:

```gherkin
Background:
* def QueueConsumer = Java.type('com.example.QueueConsumer')
* def queue = new QueueConsumer('test-queue')

# Set up message handler
* def messageHandler = function(msg) {
karate.log('Received message:', msg);
if (msg.type === 'target') {
karate.signal(msg);
}
}
* queue.listen(karate.toJava(messageHandler))

Scenario: Test message queue flow
# Send message that triggers queue activity
* url apiUrl + '/send-message'
* request { type: 'order', orderId: 'ORD-123' }
* method post
* status 200

# Wait for message on queue
* listen 5000
* def message = listenResult
* match message == { type: 'target', orderId: 'ORD-123', processed: true }
```gherkin

## WebSocket Support

### Basic WebSocket Connection

Connect to WebSocket endpoints:

```gherkin
Feature: WebSocket testing

Scenario: Basic WebSocket communication
# Create WebSocket connection
* def ws = karate.webSocket('ws://localhost:8080/socket')

# Send message
* ws.send('Hello Server')

# Wait for response
* 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' }
```gherkin

### WebSocket with Handler

Use custom handlers for message filtering:

```gherkin
Scenario: WebSocket with message filtering
# Handler that only signals for specific messages
* 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 message types)
* listen 10000
* def notification = JSON.parse(listenResult)
* match notification.type == 'notification'
* match notification.channel == 'updates'
```gherkin

### WebSocket Options

Configure WebSocket connections:

```gherkin
Scenario: WebSocket with options
* def wsOptions = {
subProtocol: 'chat',
headers: { 'Authorization': 'Bearer ' + token },
maxPayloadSize: 8388608 # 8 MB
}

* def ws = karate.webSocket('wss://secure.example.com/socket', null, wsOptions)

# Authenticated WebSocket communication
* ws.send({ action: 'getData' })
* listen 5000
* match listenResult contains { data: '#present' }
```gherkin

## Event-Driven Testing

### Multiple Event Sources

Handle multiple async event sources:

```gherkin
Background:
* def EventAggregator = Java.type('com.example.EventAggregator')
* def aggregator = new EventAggregator()

# Register multiple event sources
* def handleEvent = function(event) {
if (event.isComplete) {
karate.signal(event);
}
}
* aggregator.addSource('api', apiUrl)
* aggregator.addSource('queue', queueUrl)
* aggregator.addSource('webhook', webhookUrl)
* aggregator.start(karate.toJava(handleEvent))

Scenario: Coordinate multiple events
# Trigger distributed operation
* url apiUrl + '/start-workflow'
* request { workflowId: 'WF-123' }
* method post
* status 202

# Wait for completion from any source
* listen 30000
* def result = listenResult
* match result.workflowId == 'WF-123'
* match result.isComplete == true
```gherkin

### Event Correlation

Correlate related async events:

```gherkin
* def correlateEvents =
"""
function(correlationId, timeout) {
var events = [];
var handler = function(event) {
if (event.correlationId === correlationId) {
events.push(event);
if (events.length >= 3) { // Expecting 3 events
karate.signal(events);
}
}
};

// Register handler
karate.set('eventHandler', handler);

// Wait for all events
karate.listen(timeout);
return karate.get('listenResult');
}
"""

Scenario: Correlate multiple events
* def correlationId = java.util.UUID.randomUUID().toString()

# Start async process
* url apiUrl + '/async-process'
* request { correlationId: correlationId, steps: ['validate', 'process', 'notify'] }
* method post
* status 202

# Wait for all correlated events
* def events = correlateEvents(correlationId, 15000)
* match events == '#[3]'
* match each events[*].correlationId == correlationId
* match events[*].step == ['validate', 'process', 'notify']
```gherkin

## Timeout Strategies

### Cascading Timeouts

Implement cascading timeout strategies:

```gherkin
* def tryWithTimeouts =
"""
function(operation, timeouts) {
for (var i = 0; i < timeouts.length; i++) {
try {
karate.configure('connectTimeout', timeouts[i]);
karate.configure('readTimeout', timeouts[i]);
return operation();
} catch (e) {
karate.log('Attempt', i + 1, 'failed with timeout', timeouts[i]);
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
```gherkin

### Deadline-Based Testing

Test with absolute deadlines:

```gherkin
* def executeWithDeadline =
"""
function(operations, deadline) {
var start = Date.now();
var results = [];

for (var i = 0; i < operations.length; i++) {
var remaining = deadline - (Date.now() - start);
if (remaining <= 0) {
karate.fail('Deadline exceeded at operation ' + i);
}

karate.configure('readTimeout', remaining);
results.push(operations[i]());
}

return results;
}
"""

Scenario: Execute within deadline
* def operations = [
function() { return karate.call('step1.feature') },
function() { return karate.call('step2.feature') },
function() { return karate.call('step3.feature') }
]

# All operations must complete within 30 seconds
* def results = executeWithDeadline(operations, 30000)
* match each results[*].success == true
```gherkin

## Best Practices

### Retry Configuration

```gherkin
# ✅ Good: Reasonable retry configuration
* configure retry = { count: 5, interval: 2000 }

# ❌ Avoid: Too aggressive retry
* configure retry = { count: 100, interval: 100 }

# ✅ Good: Progressive delays
* def retryWithBackoff = { count: 5, interval: 1000, multiplier: 2 }
```gherkin

### Event Handling

```gherkin
# ✅ Good: Clear event filtering
* def handler = function(msg) {
return msg.type === 'target' && msg.status === 'ready';
}

# ✅ Good: Timeout handling
* listen 10000
* if (!listenResult) karate.fail('Event timeout')
```gherkin

### Resource Management

```gherkin
# ✅ Good: Clean up resources
Scenario: WebSocket with cleanup
* def ws = karate.webSocket(wsUrl)
* try
* ws.send('test')
* listen 5000
* finally
* ws.close()
```gherkin

## Troubleshooting

### Common Issues

```gherkin
# Issue: Retry not working
# Solution: Ensure condition returns boolean
* retry until response.status == 'ready' # Correct
* retry until response # Wrong - not a boolean

# Issue: Listen timeout too short
# Solution: Increase timeout for slow operations
* listen 30000 # 30 seconds for slow operations

# Issue: WebSocket connection drops
# Solution: Implement reconnection logic
* def connectWithRetry = function(url, maxAttempts) {
for (var i = 0; i < maxAttempts; i++) {
try {
return karate.webSocket(url);
} catch (e) {
java.lang.Thread.sleep(1000);
}
}
}
```gherkin

## Next Steps

- Learn about [Conditional Logic](/docs/advanced/conditional-logic) for complex test flows
- Explore [Hooks and Lifecycle](/docs/advanced/hooks) for test orchestration
- Understand [Debugging and Troubleshooting](/docs/advanced/debugging) techniques