EXTENSIONS
Test Doubles and Mocking
Create standalone mock servers with stateful behavior for API testing, consumer-driven contracts, and microservice development without external dependencies.
Overview
Karate provides powerful mocking capabilities that allow you to:
- Create standalone mock servers for API simulation
- Build stateful mocks that maintain data across requests
- Implement contract testing between services
- Replace external dependencies during testing
- Support microservice development with service virtualization
Request Matching
Matching Predicates
Karate provides helper functions for request matching:
Function | Description | Example |
---|---|---|
pathMatches() | Match request paths with path parameters | pathMatches('/users/{id}') |
methodIs() | Check HTTP method (recommended) | methodIs('post') |
paramExists() | Check if query parameter exists | paramExists('name') |
paramValue() | Get single query parameter value | paramValue('limit') |
typeContains() | Match Content-Type header | typeContains('xml') |
acceptContains() | Match Accept header | acceptContains('json') |
headerContains() | Check header contents | headerContains('Authorization', 'Bearer') |
bodyPath() | Query request body using JsonPath/XPath | bodyPath('$.username') |
Request Variables
Available variables for inspecting requests:
request
- Request body (parsed as JSON/XML)requestMethod
- HTTP methodrequestPath
- Request pathrequestHeaders
- All headersrequestParams
- Query parameterspathParams
- Path parameters from URL pattern
Use methodIs('post')
instead of requestMethod == 'POST'
for cleaner code.
Basic Mock Server
Simple HTTP Mock
Create a basic mock server that responds to requests:
Feature: Simple user mock
Background:
* def users = { '1': { id: 1, name: 'John Doe' } }
Scenario: pathMatches('/users/{id}') && methodIs('get')
* def user = users[pathParams.id]
* def response = user || { error: 'Not found' }
* def responseStatus = user ? 200 : 404
This mock returns predefined user data or a 404 error.
Starting the Mock Server
Start the mock server within a test:
Feature: Test with user service mock
Background:
* def mock = karate.start('user-service-mock.feature')
* url 'http://localhost:' + mock.port
Scenario: Get user by ID
Given path 'users', '1'
When method get
Then status 200
And match response == { id: 1, name: 'John Doe' }
Multiple Endpoints
Add GET and POST endpoints to your mock:
Feature: User service with multiple endpoints
Background:
* def users = { '1': { id: 1, name: 'John Doe', email: 'john@example.com' } }
Scenario: pathMatches('/users/{id}') && methodIs('get')
* def user = users[pathParams.id]
* def response = user || { error: 'User not found' }
* def responseStatus = user ? 200 : 404
Scenario: pathMatches('/users') && methodIs('post')
* def newUser = request
* def newId = (karate.sizeOf(users) + 1) + ''
* def responseStatus = 400
* if (newUser.name && newUser.email) users[newId] = karate.merge(newUser, { id: ~~newId })
* if (newUser.name && newUser.email) response = users[newId]
* if (newUser.name && newUser.email) responseStatus = 201
* if (responseStatus == 400) response = { error: 'Missing required fields' }
Stateful Mocks
Product Catalog Mock
Create a mock with shared state across requests:
Feature: Product catalog mock
Background:
* def products = {
'1': { id: 1, name: 'Laptop', price: 999, category: 'electronics' },
'2': { id: 2, name: 'Mouse', price: 29, category: 'electronics' },
'3': { id: 3, name: 'Desk', price: 299, category: 'furniture' }
}
Scenario: pathMatches('/products') && methodIs('get')
* def category = paramValue('category')
* def allProducts = karate.valuesOf(products)
* def filtered = category ? allProducts.filter(p => p.category == category) : allProducts
* def response = { products: filtered, total: filtered.length }
Scenario: pathMatches('/products/{id}') && methodIs('get')
* def product = products[pathParams.id]
* def response = product || { error: 'Product not found' }
* def responseStatus = product ? 200 : 404
Shopping Cart Mock
Manage cart state across multiple operations:
Feature: Shopping cart mock
Background:
* def cart = {}
Scenario: pathMatches('/cart') && methodIs('post')
* def cartData = request
* def userId = cartData.userId
* if (!cart[userId]) cart[userId] = []
* def existingItem = cart[userId].find(item => item.productId == cartData.productId)
* if (existingItem) existingItem.quantity += cartData.quantity
* if (!existingItem) cart[userId].push({ productId: cartData.productId, quantity: cartData.quantity })
* def response = { message: 'Item added', items: cart[userId].length }
* def responseStatus = 201
Scenario: pathMatches('/cart/{userId}') && methodIs('get')
* def userCart = cart[pathParams.userId] || []
* def response = { items: userCart, total: userCart.length }
Order Management Mock
Create orders with stateful tracking:
Feature: Order management mock
Background:
* def orders = {}
Scenario: pathMatches('/orders') && methodIs('post')
* def orderData = request
* def orderId = 'order-' + java.util.UUID.randomUUID()
* def order = {
id: orderId,
userId: orderData.userId,
items: orderData.items,
status: 'pending',
created: new Date().toISOString()
}
* orders[orderId] = order
* def response = order
* def responseStatus = 201
Scenario: pathMatches('/orders/{id}') && methodIs('get')
* def order = orders[pathParams.id]
* def response = order || { error: 'Order not found' }
* def responseStatus = order ? 200 : 404
Response Configuration
Status Codes and Headers
Control mock responses with configuration:
Scenario: pathMatches('/api/data')
* def response = { message: 'Success' }
* def responseStatus = 201
* def responseHeaders = { 'X-Request-Id': 'abc123' }
Simulating Latency
Add realistic network delays:
Scenario: pathMatches('/slow-api')
* def responseDelay = 2000
* def response = { data: 'delayed' }
Response delays are specified in milliseconds.
Contract Testing
Provider Contract Mock
Define contracts for API providers with schema validation:
Feature: Payment provider contract
Background:
* def contractSchema = {
request: { amount: 'number', currency: 'string', cardNumber: 'string' },
response: {
success: { transactionId: 'string', status: 'approved', amount: 'number' },
failure: { error: 'string', code: 'string' }
}
}
Scenario: pathMatches('/payments') && methodIs('post')
* def paymentRequest = request
* match paymentRequest == contractSchema.request
* def isDeclined = paymentRequest.cardNumber.endsWith('0000')
* def transactionId = 'txn-' + java.util.UUID.randomUUID()
* if (isDeclined) response = { error: 'Payment declined', code: 'CARD_DECLINED' }
* if (isDeclined) responseStatus = 402
* if (!isDeclined) response = { transactionId: transactionId, status: 'approved', amount: paymentRequest.amount }
* if (!isDeclined) responseStatus = 200
* def expectedSchema = isDeclined ? contractSchema.response.failure : contractSchema.response.success
* match response == expectedSchema
Proxy Mode
Forwarding Requests
Use karate.proceed()
to forward requests to real services:
Scenario: pathMatches('/api/v2')
* karate.proceed('https://backend-service.com')
Modifying Proxied Responses
Intercept and modify responses:
Scenario: pathMatches('/api/data')
* karate.proceed('https://api.example.com')
* def response = karate.merge(response, { cached: true })
Advanced Features
Conditional Termination
Use karate.abort()
to stop scenario execution:
Scenario: pathMatches('/protected')
* def authorized = karate.request.header('authorization')
* if (!authorized) karate.abort()
* def response = { message: 'authorized' }
Aborted scenarios return no response, resulting in connection timeout for the client.
Authentication Mock
Create mocks with session management:
Feature: Authentication mock
Background:
* def users = { 'user@example.com': { id: 1, password: 'secret123', name: 'John' } }
* def sessions = {}
Scenario: pathMatches('/auth/login') && methodIs('post')
* def credentials = request
* def user = users[credentials.email]
* def validPassword = user && user.password == credentials.password
* if (validPassword) def token = 'session-' + java.util.UUID.randomUUID()
* if (validPassword) sessions[token] = { userId: user.id, expires: new Date().getTime() + 3600000 }
* if (validPassword) response = { token: token, user: { id: user.id, name: user.name } }
* if (validPassword) responseStatus = 200
* if (!validPassword) response = { error: 'Invalid credentials' }
* if (!validPassword) responseStatus = 401
Scenario: pathMatches('/profile') && methodIs('get')
* def token = karate.request.header('authorization').replace('Bearer ', '')
* def session = sessions[token]
* def isValid = session && session.expires > new Date().getTime()
* if (!isValid) response = { error: 'Unauthorized' }
* if (!isValid) responseStatus = 401
* if (isValid) response = { id: session.userId, name: users[session.userId].name }
* if (isValid) responseStatus = 200
Error Simulation
Simulate various error conditions and edge cases:
Feature: Error simulation mock
Background:
* def errorRate = 0.1
* def requestCount = 0
Scenario: pathMatches('/api/unreliable') && methodIs('get')
* requestCount = requestCount + 1
* def shouldError = Math.random() < errorRate
* if (shouldError) def errorTypes = ['timeout', 'server_error', 'rate_limit']
* if (shouldError) def errorType = errorTypes[~~(Math.random() * errorTypes.length)]
* if (shouldError) responseStatus = errorType == 'timeout' ? 504 : errorType == 'rate_limit' ? 429 : 500
* if (shouldError) response = { error: 'Service temporarily unavailable', type: errorType }
* if (!shouldError) response = { message: 'Success', requestNumber: requestCount }
* if (!shouldError) responseStatus = 200
Background Lifecycle
In mock servers, Background
runs once on startup, not before each scenario.
Use Background for:
- Reading files
- Setting up common functions
- Initializing global state
- Configuring CORS and headers
CORS Support
Enable CORS for browser-based testing:
Background:
* configure cors = true
For custom CORS headers:
Background:
* configure responseHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE'
}
State Reset for Testing
Provide endpoints to reset mock state:
Background:
* def users = {}
* def orders = {}
Scenario: pathMatches('/__test/reset') && methodIs('post')
* users = {}
* orders = {}
* def response = { message: 'State reset successfully' }
* def responseStatus = 200
Call this endpoint between tests to ensure clean state.
Deployment and Integration
Standalone Mock Server
Run mocks as standalone servers with full configuration:
# Basic server
java -jar karate.jar -m mock.feature -p 8080
# With SSL (auto-generates certificate)
java -jar karate.jar -m mock.feature -p 8443 -s
# Custom SSL certificate
java -jar karate.jar -m mock.feature -p 8443 -s -c cert.pem -k key.pem
# With environment
java -jar karate.jar -m mock.feature -p 8080 -e test
# Watch mode (hot-reload)
java -jar karate.jar -m mock.feature -p 8080 -W
Command-Line Options
Option | Description |
---|---|
-m | Mock feature file |
-p | Port number |
-s | Enable SSL |
-c | SSL certificate (PEM) |
-k | SSL private key (PEM) |
-e | Environment |
-W | Watch mode (hot-reload) |
CI/CD Integration
Integrate mocks into your build pipeline:
# Docker Compose for testing
version: '3.8'
services:
user-service-mock:
image: karatelabs/karate
command: -m /mocks/user-service.feature -p 8080
ports:
- '8080:8080'
volumes:
- ./mocks:/mocks
app-tests:
image: maven:3.8-openjdk-11
depends_on:
- user-service-mock
environment:
- USER_SERVICE_URL=http://user-service-mock:8080
command: mvn test -Dtest=IntegrationTests
Best Practices
Mock Design Principles
- Keep mocks focused: Each mock should represent a single service or bounded context
- Use realistic data: Populate mocks with data that closely resembles production
- Implement proper error handling: Include edge cases and error scenarios
- Version your contracts: Maintain backward compatibility when updating mocks
Mock state must use pure JSON objects. Avoid Java objects or functions that cannot be serialized.
Performance Considerations
Cache responses for improved performance:
Feature: Performance-optimized mock
Background:
* def cache = {}
* def maxCacheSize = 1000
Scenario: pathMatches('/api/data/{id}') && methodIs('get')
* def dataId = pathParams.id
* def cachedData = cache[dataId]
* if (cachedData) response = cachedData
* if (cachedData) responseStatus = 200
* if (!cachedData) def data = { id: dataId, value: 'generated-' + dataId, timestamp: new Date().toISOString() }
* if (!cachedData && karate.sizeOf(cache) >= maxCacheSize) def oldestKey = karate.keysOf(cache)[0]
* if (!cachedData && karate.sizeOf(cache) >= maxCacheSize) cache[oldestKey] = null
* if (!cachedData) cache[dataId] = data
* if (!cachedData) response = data
* if (!cachedData) responseStatus = 200
For high-volume mocking, consider caching responses and implementing state cleanup strategies.
Testing Strategies
- Contract verification: Test that your mocks adhere to defined API contracts
- State consistency: Verify that stateful operations maintain data integrity
- Performance testing: Use mocks to simulate load and measure system behavior
- Chaos engineering: Introduce controlled failures to test resilience
Mock servers are thread-safe. Each test thread can access shared state without race conditions.
Next Steps
- Load test your APIs: Performance Testing
- Extend Karate functionality: Actions and Hooks
- Configure parallel test execution: Parallel Execution
- Build reusable test components: Calling Features
- Debug mock servers: Debugging and Troubleshooting