Skip to main content

EXTENSIONS

Test Doubles and Mocking

What You'll Learn

  • Create mock servers for API testing without dependencies
  • Implement contract testing with consumer-provider patterns
  • Build stateful mocks with dynamic responses
  • Use path and request matching for flexible routing
  • Deploy standalone mock servers for team collaboration
  • Integrate mocks within test scenarios for isolation

Overview

Karate's mocking capabilities through karate-netty provide powerful service virtualization features. Create realistic test doubles that can simulate complex API behaviors, enabling true isolation testing and contract testing between consumers and providers.

Getting Started with Mocks

Basic Mock Server

Create a simple mock server:

Feature: Basic mock server

Background:
* def port = 8080
* def mock = karate.start({ mock: 'classpath:mocks/simple-mock.feature', port: port })

Scenario: Test with mock
* url 'http://localhost:' + port
* path '/users/1'
* method get
* status 200
* match response == { id: 1, name: 'John Doe' }

# Stop mock after test
* karate.stop(port)
```gherkin

### Mock Feature Definition

Define mock behavior in a feature file:

```gherkin
Feature: Simple mock responses

Background:
* def users = { '1': { id: 1, name: 'John Doe' }, '2': { id: 2, name: 'Jane Smith' } }

Scenario: pathMatches('/users/{id}')
* def userId = pathParams.id
* def user = users[userId]
* def response = user || { error: 'User not found' }
* def responseStatus = user ? 200 : 404

Scenario: pathMatches('/users') && methodIs('post')
* def newUser = request
* def response = karate.merge(newUser, { id: ~~(Math.random() * 1000), created: new Date() })
* def responseStatus = 201
```gherkin

## Request Matching

### Path Matching

Use patterns to match request paths:

```gherkin
Feature: Advanced path matching

Scenario: pathMatches('/api/v{version}/users')
* def apiVersion = pathParams.version
* def response = { version: apiVersion, users: [] }

Scenario: pathMatches('/products/{category}/{id}')
* def category = pathParams.category
* def productId = pathParams.id
* def response = { category: category, id: productId, name: 'Product ' + productId }

Scenario: pathMatches('/search') && paramExists('q')
* def query = paramValue('q')
* def results = karate.call('search-logic.feature', { searchTerm: query })
* def response = results.response
```gherkin

### Request Body Matching

Match based on request content:

```gherkin
Feature: Request body matching

Scenario: requestMatches({ type: 'CREATE' })
* def response = { id: uuid(), status: 'created', timestamp: now() }
* def responseStatus = 201

Scenario: requestMatches({ action: 'DELETE', id: '#number' })
* def itemId = request.id
* def response = { message: 'Item ' + itemId + ' deleted' }
* def responseStatus = 204

Scenario: requestHeaderContains('Content-Type', 'application/xml')
* def xmlResponse = <response><status>OK</status></response>
* def response = xmlResponse
* def responseHeaders = { 'Content-Type': 'application/xml' }
```gherkin

## Stateful Mocks

### Managing State

Create mocks that maintain state:

```gherkin
Feature: Stateful mock server

Background:
* def storage = {}
* def counter = { value: 0 }

Scenario: pathMatches('/counter/increment') && methodIs('post')
* set counter.value = counter.value + 1
* def response = { count: counter.value }

Scenario: pathMatches('/storage/{key}') && methodIs('put')
* def key = pathParams.key
* set storage[key] = request
* def response = { stored: true, key: key }

Scenario: pathMatches('/storage/{key}') && methodIs('get')
* def key = pathParams.key
* def data = storage[key]
* def response = data || { error: 'Not found' }
* def responseStatus = data ? 200 : 404

Scenario: pathMatches('/storage') && methodIs('delete')
* def storage = {}
* def response = { message: 'Storage cleared' }
```gherkin

### Session Management

Handle session-based interactions:

```gherkin
Feature: Session-aware mock

Background:
* def sessions = {}

* def getSession =
"""
function() {
var sessionId = karate.request.header('session-id');
if (!sessionId) {
sessionId = java.util.UUID.randomUUID().toString();
karate.set('responseHeaders', { 'session-id': sessionId });
}
if (!sessions[sessionId]) {
sessions[sessionId] = { id: sessionId, data: {} };
}
return sessions[sessionId];
}
"""

Scenario: pathMatches('/session/data')
* def session = getSession()

* if (methodIs('get')) karate.set('response', session.data)
* if (methodIs('put')) session.data = request
* if (methodIs('delete')) session.data = {}
```gherkin

## Dynamic Responses

### Conditional Responses

Return different responses based on conditions:

```gherkin
Feature: Dynamic response generation

Background:
* def scenarios =
"""
{
success: { status: 'success', data: { id: 1, value: 'test' } },
error: { status: 'error', message: 'Something went wrong' },
timeout: null
}
"""

Scenario: pathMatches('/api/test')
* def scenario = paramValue('scenario') || 'success'

* if (scenario == 'timeout') karate.abort()

* def response = scenarios[scenario]
* def responseStatus = scenario == 'error' ? 500 : 200
* def responseDelay = scenario == 'slow' ? 5000 : 0
```gherkin

### Data Generation

Generate realistic test data:

```gherkin
Feature: Test data generation

Background:
* def faker = Java.type('com.github.javafaker.Faker')
* def fake = new faker()

* def generateUser =
"""
function() {
return {
id: ~~(Math.random() * 10000),
name: fake.name().fullName(),
email: fake.internet().emailAddress(),
phone: fake.phoneNumber().phoneNumber(),
address: {
street: fake.address().streetAddress(),
city: fake.address().city(),
country: fake.address().country()
}
};
}
"""

Scenario: pathMatches('/users/random')
* def count = ~~paramValue('count') || 1
* def users = karate.repeat(count, generateUser)
* def response = count == 1 ? users[0] : users
```gherkin

## Contract Testing

### Provider Mock

Define provider contracts:

```gherkin
Feature: Payment service provider contract

Background:
* def validCards = ['4111111111111111', '5500000000000004']

Scenario: pathMatches('/payment/authorize') && methodIs('post')
* def card = request.cardNumber
* def amount = request.amount

# Validate request
* assert card && card.length == 16
* assert amount && amount > 0

# Process based on card
* def isValid = validCards.contains(card)
* def response = isValid
? { authorized: true, transactionId: uuid(), amount: amount }
: { authorized: false, reason: 'Invalid card' }
* def responseStatus = isValid ? 200 : 400
```gherkin

### Consumer Testing

Test against provider contracts:

```gherkin
Feature: Payment service consumer

Background:
# Start provider mock
* def mock = karate.start({ mock: 'classpath:mocks/payment-provider.feature', port: 8080 })

Scenario: Successful payment
* url 'http://localhost:8080/payment/authorize'
* request { cardNumber: '4111111111111111', amount: 100.00 }
* method post
* status 200
* match response == { authorized: true, transactionId: '#uuid', amount: 100.00 }

Scenario: Failed payment
* url 'http://localhost:8080/payment/authorize'
* request { cardNumber: '0000000000000000', amount: 100.00 }
* method post
* status 400
* match response == { authorized: false, reason: 'Invalid card' }
```gherkin

## Advanced Mocking

### Response Headers

Configure custom response headers:

```gherkin
Feature: Custom headers

Background:
* configure responseHeaders = { 'X-API-Version': '2.0', 'X-Request-Id': '#(uuid())' }

Scenario: pathMatches('/api/data')
* def response = { data: 'test' }
* def responseHeaders =
"""
{
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'X-Response-Time': '#(responseTime)'
}
"""
```gherkin

### CORS Configuration

Enable CORS for mock servers:

```gherkin
Feature: CORS-enabled mock

Background:
* configure cors = true

# Or with specific configuration
* configure cors =
"""
{
allowOrigin: 'http://localhost:3000',
allowMethods: 'GET, POST, PUT, DELETE',
allowHeaders: 'Content-Type, Authorization',
maxAge: 3600
}
"""

Scenario: pathMatches('/api/resource')
* def response = { resource: 'data' }
```gherkin

### Error Simulation

Simulate various error conditions:

```gherkin
Feature: Error simulation

Background:
* def errorRate = 0.2 # 20% error rate

* def shouldFail = function() {
return Math.random() < errorRate;
}

Scenario: pathMatches('/unreliable/service')
* def fail = shouldFail()

* if (fail) karate.set('responseStatus', 503)
* if (fail) karate.set('response', { error: 'Service temporarily unavailable' })
* if (!fail) karate.set('response', { data: 'Success' })

# Add retry hint
* if (fail) karate.set('responseHeaders', { 'Retry-After': '5' })
```gherkin

## Standalone Mock Server

### Deployment

Run mock as standalone server:

```bash
# Using karate.jar
java -jar karate.jar -m mock.feature -p 8080

# Using Docker
docker run --rm -p 8080:8080 -v "$PWD":/src karatelabs/karate \
java -jar /karate.jar -m /src/mock.feature -p 8080
```gherkin

### Configuration File

Create a mock configuration:

```javascript
// mock-config.js
function fn() {
return {
port: karate.properties['mock.port'] || 8080,
ssl: karate.properties['mock.ssl'] === 'true',
cert: 'classpath:certs/mock.pem',
key: 'classpath:certs/mock.key',
cors: true,
responseHeaders: {
'X-Mock-Server': 'Karate'
}
};
}
```gherkin

## Testing with Mocks

### Parallel Testing

Use mocks in parallel test execution:

```gherkin
Feature: Parallel testing with mocks

Background:
# Each thread gets its own mock port
* def mockPort = karate.properties['mock.port'] || (8080 + karate.info.scenarioIndex)
* def mock = karate.start({ mock: 'classpath:mock.feature', port: mockPort })
* url 'http://localhost:' + mockPort

Scenario: Test 1
* path '/test1'
* method get
* status 200

Scenario: Test 2
* path '/test2'
* method get
* status 200
```gherkin

### Mock Lifecycle

Manage mock lifecycle properly:

```gherkin
Feature: Mock lifecycle management

Background:
* def mockPort = 8090

* def startMock =
"""
function() {
try {
return karate.start({
mock: 'classpath:mock.feature',
port: mockPort,
arg: { initialData: read('test-data.json') }
});
} catch (e) {
karate.log('Mock start failed:', e.message);
return null;
}
}
"""

* def mock = startMock()
* assert mock != null

# Configure cleanup
* configure afterScenario =
"""
function() {
if (karate.get('mock')) {
karate.stop(karate.get('mockPort'));
}
}
"""

Scenario: Use mock
* url 'http://localhost:' + mockPort
* path '/api/test'
* method get
* status 200
```gherkin

## Best Practices

### Mock Design

```gherkin
# ✅ Good: Clear, focused mock scenarios
Scenario: pathMatches('/users/{id}') && methodIs('get')
* def response = { id: pathParams.id, name: 'Test User' }

# ❌ Avoid: Overly complex mock logic
Scenario: pathMatches('/api')
# Too much logic in one scenario
```gherkin

### Response Timing

```gherkin
# ✅ Good: Realistic response delays
Scenario: pathMatches('/slow/endpoint')
* def responseDelay = 500 # Simulate network latency
* def response = { data: 'delayed response' }

# ✅ Good: Variable delays
* def responseDelay = 100 + ~~(Math.random() * 400)
```gherkin

## Troubleshooting

### Common Issues

```gherkin
# Issue: Port already in use
# Solution: Use dynamic port allocation
* def mock = karate.start({ mock: 'mock.feature', port: 0 })
* def actualPort = mock.port

# Issue: Mock not stopping
# Solution: Ensure cleanup in afterScenario
* configure afterScenario = function() { karate.stop(mockPort); }

# Issue: State not persisting
# Solution: Use Background variables
Background:
* def state = {} # Shared across scenarios in mock
```gherkin

## Next Steps

- Explore [Performance Testing](/docs/extensions/performance-testing) with mocks
- Learn about [UI Testing](/docs/extensions/ui-testing) with mock backends
- Review [Examples and Demos](/docs/extensions/examples) for real-world mock scenarios