Skip to main content

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:

FunctionDescriptionExample
pathMatches()Match request paths with path parameterspathMatches('/users/{id}')
methodIs()Check HTTP method (recommended)methodIs('post')
paramExists()Check if query parameter existsparamExists('name')
paramValue()Get single query parameter valueparamValue('limit')
typeContains()Match Content-Type headertypeContains('xml')
acceptContains()Match Accept headeracceptContains('json')
headerContains()Check header contentsheaderContains('Authorization', 'Bearer')
bodyPath()Query request body using JsonPath/XPathbodyPath('$.username')

Request Variables

Available variables for inspecting requests:

  • request - Request body (parsed as JSON/XML)
  • requestMethod - HTTP method
  • requestPath - Request path
  • requestHeaders - All headers
  • requestParams - Query parameters
  • pathParams - Path parameters from URL pattern
Best Practice

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

Important

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

OptionDescription
-mMock feature file
-pPort number
-sEnable SSL
-cSSL certificate (PEM)
-kSSL private key (PEM)
-eEnvironment
-WWatch 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

  1. Keep mocks focused: Each mock should represent a single service or bounded context
  2. Use realistic data: Populate mocks with data that closely resembles production
  3. Implement proper error handling: Include edge cases and error scenarios
  4. Version your contracts: Maintain backward compatibility when updating mocks
Pure JSON State

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
Performance

For high-volume mocking, consider caching responses and implementing state cleanup strategies.

Testing Strategies

  1. Contract verification: Test that your mocks adhere to defined API contracts
  2. State consistency: Verify that stateful operations maintain data integrity
  3. Performance testing: Use mocks to simulate load and measure system behavior
  4. Chaos engineering: Introduce controlled failures to test resilience
Parallel Execution

Mock servers are thread-safe. Each test thread can access shared state without race conditions.

Next Steps