Mock Servers for API Testing: Simulating External Dependencies
In this tutorial, you will learn about Mock Servers for API Testing: Simulating External Dependencies. We cover key concepts, practical examples, and best practices to help you master this topic.
Mock servers simulate external API dependencies during testing, providing controlled responses, error simulation, latency injection, and record-replay capabilities for reliable, deterministic test execution.
What You'll Learn
How to use mock servers for API testing, strategies (static, dynamic, record-replay), tools (WireMock, MockServer, Postman Mock Server, Mountebank), simulate errors and latency, verify request expectations, and integrate mocks into CI/CD.
Why It Matters
Tests that call real external APIs are slow, flaky, and expensive. Mock servers provide fast, deterministic, isolated testing. DodaTech uses WireMock to simulate payment gateway, SMS provider, and email service responses in tests.
Real-World Use
A DodaTech test calls the payment API which depends on Stripe. WireMock intercepts the Stripe call and returns a mock success response. The test completes in 100ms instead of 2s, and works offline without Stripe credentials.
flowchart LR
A["Your App\nUnder Test"] --> B["WireMock\nMock Server"]
B --> C["Stubbed\nResponse"]
A --> D["Database\n(real)"]
B --> E{"Mapping\nMatch?"}
E -->|Match| F["Return\nStubbed Data"]
E -->|No Match| G["Proxy to\nReal API"]
style A fill:#dbeafe,stroke:#2563eb
style B fill:#bbf7d0,stroke:#16a34a
style G fill:#fef3c7,stroke:#d97706
WireMock Setup
# WireMock can run as standalone JAR or embedded in tests
# Install: java -jar wiremock-standalone-3.x.jar --port 8080
# Python test with WireMock (via HTTP admin API)
import requests
import json
WIREMOCK_URL = "http://localhost:8080"
def setup_mock_stripe_charge():
"""Stub the Stripe charge API."""
mapping = {
"request": {
"method": "POST",
"url": "/v1/charges",
"headers": {
"Authorization": {
"matches": "Bearer sk_test_.*"
}
}
},
"response": {
"status": 200,
"json_body": {
"id": "ch_mock_123",
"object": "charge",
"amount": 2999,
"currency": "usd",
"status": "succeeded",
"outcome": {"network_status": "approved_by_network"}
},
"headers": {
"Content-Type": "application/json"
}
}
}
resp = requests.post(
f"{WIREMOCK_URL}/__admin/mappings",
json=mapping
)
print(f"Stub created: {resp.status_code}")
return mapping
setup_mock_stripe_charge()
# Now your app's Stripe calls go to WireMock instead of Stripe
# Your app would use: stripe.api_base = "http://localhost:8080"
WireMock Dynamic Responses
# Dynamic response based on request body
def setup_dynamic_stub():
mapping = {
"request": {
"method": "POST",
"url": "/v1/charges"
},
"response": {
"status": 200,
"json_body": {
"id": "{{randomValue length=20 type='ALPHANUMERIC'}}",
"amount": "{{jsonPath request.body '$.amount'}}",
"status": "{{#unless request.body.card}}failed{{else}}succeeded{{/unless}}",
"created": "{{now}}"
},
"transformers": ["response-template"]
}
}
requests.post(f"{WIREMOCK_URL}/__admin/mappings", json=mapping)
setup_dynamic_stub()
# Verify request was made to WireMock
def verify_mock_called():
resp = requests.get(f"{WIREMOCK_URL}/__admin/requests")
requests_data = resp.json()
print(f"Requests made to mock: {len(requests_data['requests'])}")
for req in requests_data["requests"]:
print(f" {req['request']['method']} {req['request']['url']}")
# verify_mock_called()
# Expected output:
# Requests made to mock: 1
# POST /v1/charges
Simulating Errors and Latency
def setup_error_scenarios():
"""Configure different error responses."""
# 500 Internal Server Error
error_500 = {
"request": {"method": "GET", "url": "/v1/payments/unstable"},
"response": {
"status": 500,
"json_body": {
"error": {"type": "api_error", "message": "Internal server error"}
}
}
}
requests.post(f"{WIREMOCK_URL}/__admin/mappings", json=error_500)
# Timeout simulation (fixed delay)
timeout_mapping = {
"request": {"method": "GET", "url": "/v1/payments/slow"},
"response": {
"status": 200,
"json_body": {"status": "ok"},
"fixedDelayMilliseconds": 30000 # 30 second delay
}
}
requests.post(f"{WIREMOCK_URL}/__admin/mappings", json=timeout_mapping)
# Rate limiting
rate_limit = {
"request": {"method": "POST", "url": "/v1/charges"},
"response": {
"status": 429,
"json_body": {
"error": {"type": "rate_limit", "message": "Too many requests"}
},
"headers": {"Retry-After": "5"}
}
}
requests.post(f"{WIREMOCK_URL}/__admin/mappings", json=rate_limit)
# Connection reset (simulate network failure)
socket_reset = {
"request": {"method": "GET", "url": "/v1/payments/unreachable"},
"response": {
"fault": "CONNECTION_RESET_BY_PEER"
}
}
requests.post(f"{WIREMOCK_URL}/__admin/mappings", json=socket_reset)
setup_error_scenarios()
print("Error scenarios configured")
# Test your app's error handling
# Your app should handle 500, timeout, 429, and connection reset gracefully
Record-Replay Pattern
def setup_record_replay():
"""
Record mode: proxy to real API and save responses.
Replay mode: serve saved responses without real API.
"""
# Record mode - proxy unmatched requests to real API
record_config = {
"request": {"method": "ANY", "urlPattern": "/api/.*"},
"response": {
"proxyBaseUrl": "https://api.real-service.com"
},
"persistent": True
}
# In WireMock: responses are automatically recorded when using proxy
# Switch to stub mode after recording
# WireMock serves recorded responses without hitting the real API
print("Record-replay pattern:")
print(" 1. Start with proxy to record real responses")
print(" 2. Run tests once to capture responses")
print(" 3. Switch to stub mode (remove proxy mapping)")
print(" 4. Tests now use recorded responses offline")
# For Python tests, you can use responses library for record-replay
# Or VCR.py for recording HTTP interactions
MockServer Alternative
# MockServer (Java) provides similar functionality
# Can be run as Docker container or embedded
# MockServer with Docker:
# docker run -d -p 1080:1080 mockserver/mockserver
# Python client for MockServer
import requests
MOCKSERVER_URL = "http://localhost:1080"
def setup_mockserver_expectation():
expectation = {
"httpRequest": {
"method": "POST",
"path": "/api/external/users",
"headers": {
"Authorization": {"values": ["Bearer test_token"]}
}
},
"httpResponse": {
"statusCode": 201,
"body": json.dumps({
"id": "mock-user-123",
"email": "mock@example.com"
}),
"headers": {
"Content-Type": ["application/json"]
}
},
"times": {
"unlimited": True
}
}
resp = requests.put(
f"{MOCKSERVER_URL}/mockserver/expectation",
json=expectation
)
print(f"MockServer expectation: {resp.status_code}")
# setup_mockserver_expectation()
Common Mistakes
1. Mocking Too Much
Mocking every dependency creates tests that pass but don't validate real behavior. Mock external services (payment gateways, SMS providers) but use real databases and real internal Microservices.
2. Static Mock Responses That Never Change
A mock that always returns success hides integration issues. Rotate responses: first call returns success, second returns 500, third returns timeout. Test all code paths.
3. Not Verifying Mock Interactions
Just because you set up a mock doesn't mean your code called it. Always verify that the expected request was made: verify_mock_called() or assertions on WireMock request counts.
4. Forgetting to Reset Mocks Between Tests
Mock state persists across tests. Unreset mocks cause test pollution where one test's setup affects another. Reset all mocks in @BeforeEach/setup_method.
5. Using Mocks That Don't Match Real API Behavior
Mocks that return unrealistic data (wrong field names, incorrect types, impossible status codes) cause false confidence. Keep mocks aligned with the real API contract.
Practice Questions
- What is the purpose of a mock server in API testing?
- How do you simulate network failures with WireMock?
- What is record-replay pattern?
- How do you verify that your code called the mock correctly?
Answers:
- A mock server simulates external API dependencies, providing controlled responses, error simulation, and latency injection. This makes tests fast, deterministic, and independent of external service availability.
- WireMock supports faults:
CONNECTION_RESET_BY_PEER,EMPTY_RESPONSE,MALFORMED_RESPONSE_CHUNK, andRANDOM_DATA_THEN_CLOSE. Use.withFault()in Java or"fault": "..."in JSON. - Record-replay captures real API responses during a recording run, then serves those responses during testing without hitting the real API. Tools: WireMock proxy, VCR.py (Python), Betamax.
- Use WireMock's request journal:
GET /__admin/requestsreturns all received requests. Assert on count, method, URL, headers, and body. Verify specific requests were made with expected parameters.
Challenge: Set up WireMock for a multi-service application: create mocks for 3 external APIs (payment, SMS, email), configure error scenarios (500, timeout, rate limit, connection reset) for each, use record-replay for a stable API, verify all mock interactions, and write tests that pass both online and offline.
FAQ
Mini Project
Build a complete mock server setup for an e-commerce app: WireMock stubs for payment (Stripe), SMS (Twilio), and email (SendGrid) services, stateful flows (create charge -> refund), error scenarios for each service, record-replay for development, verify all interactions in test assertions, and run both online and offline.
What's Next
Contract Testing — verify API contracts between services with Pact.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro