Skip to content

Integration Testing Example

This guide shows how to use mockd in integration tests for various languages and frameworks.

mockd is ideal for integration testing because:

  • Isolation: Tests don’t depend on external services
  • Speed: No network latency to real APIs
  • Predictability: Responses are always consistent
  • Control: Easy to simulate errors and edge cases
Terminal window
# Start in background
mockd start --config test-mocks.json &
MOCKD_PID=$!
# Run tests
npm test
# Cleanup
kill $MOCKD_PID
Terminal window
curl -X DELETE http://localhost:4290/state
Terminal window
API_BASE_URL=http://localhost:4280 npm test

jest.setup.js:

const { spawn } = require('child_process');
let mockdProcess;
beforeAll(async () => {
// Start mockd
mockdProcess = spawn('mockd', ['start', '--config', 'test-mocks.json'], {
stdio: 'pipe'
});
// Wait for server to be ready
await waitForServer('http://localhost:4280/health');
});
afterAll(() => {
if (mockdProcess) {
mockdProcess.kill();
}
});
beforeEach(async () => {
// Reset state
await fetch('http://localhost:4290/state', { method: 'DELETE' });
});
async function waitForServer(url, timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
await fetch(url);
return;
} catch {
await new Promise(r => setTimeout(r, 100));
}
}
throw new Error('Server did not start');
}
const API = process.env.API_BASE_URL || 'http://localhost:4280';
describe('User Service', () => {
test('fetches user by ID', async () => {
const response = await fetch(`${API}/api/users/1`);
const user = await response.json();
expect(response.status).toBe(200);
expect(user).toEqual({
id: 1,
name: 'Alice',
email: 'alice@example.com'
});
});
test('handles user not found', async () => {
const response = await fetch(`${API}/api/users/999`);
expect(response.status).toBe(404);
});
test('creates new user', async () => {
const response = await fetch(`${API}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Charlie',
email: 'charlie@example.com'
})
});
expect(response.status).toBe(201);
const user = await response.json();
expect(user.id).toBeDefined();
expect(user.name).toBe('Charlie');
});
});
describe('Error Handling', () => {
test('handles server errors gracefully', async () => {
// Add temporary mock for error
await fetch('http://localhost:4290/mocks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request: { method: 'GET', path: '/api/flaky' },
response: { status: 500, body: { error: 'Internal error' } }
})
});
const response = await fetch(`${API}/api/flaky`);
expect(response.status).toBe(500);
// Test your app's error handling
});
test('handles timeout', async () => {
await fetch('http://localhost:4290/mocks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request: { method: 'GET', path: '/api/slow' },
response: { status: 200, delay: '10s', body: {} }
})
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1000);
await expect(
fetch(`${API}/api/slow`, { signal: controller.signal })
).rejects.toThrow();
clearTimeout(timeout);
});
});

conftest.py:

import subprocess
import time
import requests
import pytest
@pytest.fixture(scope="session")
def mockd_server():
"""Start mockd server for the test session."""
proc = subprocess.Popen(
["mockd", "start", "--config", "test-mocks.json"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# Wait for server
for _ in range(50):
try:
requests.get("http://localhost:4280/health")
break
except requests.ConnectionError:
time.sleep(0.1)
else:
raise RuntimeError("mockd failed to start")
yield "http://localhost:4280"
proc.terminate()
proc.wait()
@pytest.fixture(autouse=True)
def reset_state():
"""Reset mockd state before each test."""
requests.delete("http://localhost:4290/state")
import requests
import pytest
def test_get_users(mockd_server):
response = requests.get(f"{mockd_server}/api/users")
assert response.status_code == 200
users = response.json()
assert len(users) >= 1
def test_create_user(mockd_server):
response = requests.post(
f"{mockd_server}/api/users",
json={"name": "Test User", "email": "test@example.com"}
)
assert response.status_code == 201
user = response.json()
assert "id" in user
assert user["name"] == "Test User"
def test_user_not_found(mockd_server):
response = requests.get(f"{mockd_server}/api/users/99999")
assert response.status_code == 404
class TestStatefulOperations:
def test_crud_workflow(self, mockd_server):
# Create
create_resp = requests.post(
f"{mockd_server}/api/tasks",
json={"title": "Test task", "status": "todo"}
)
assert create_resp.status_code == 201
task_id = create_resp.json()["id"]
# Read
get_resp = requests.get(f"{mockd_server}/api/tasks/{task_id}")
assert get_resp.json()["title"] == "Test task"
# Update
requests.patch(
f"{mockd_server}/api/tasks/{task_id}",
json={"status": "done"}
)
get_resp = requests.get(f"{mockd_server}/api/tasks/{task_id}")
assert get_resp.json()["status"] == "done"
# Delete
delete_resp = requests.delete(f"{mockd_server}/api/tasks/{task_id}")
assert delete_resp.status_code == 204
package integration_test
import (
"encoding/json"
"net/http"
"os/exec"
"testing"
"time"
)
var baseURL = "http://localhost:4280"
var adminURL = "http://localhost:4290"
func TestMain(m *testing.M) {
// Start mockd
cmd := exec.Command("mockd", "start", "--config", "test-mocks.json")
if err := cmd.Start(); err != nil {
panic(err)
}
// Wait for ready
waitForServer(baseURL + "/health")
// Run tests
code := m.Run()
// Cleanup
cmd.Process.Kill()
os.Exit(code)
}
func waitForServer(url string) {
for i := 0; i < 50; i++ {
if _, err := http.Get(url); err == nil {
return
}
time.Sleep(100 * time.Millisecond)
}
panic("server did not start")
}
func resetState(t *testing.T) {
req, _ := http.NewRequest("DELETE", adminURL+"/state", nil)
http.DefaultClient.Do(req)
}
func TestGetUsers(t *testing.T) {
resetState(t)
resp, err := http.Get(baseURL + "/api/users")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("expected 200, got %d", resp.StatusCode)
}
var users []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&users)
if len(users) == 0 {
t.Error("expected users")
}
}
func TestCreateTask(t *testing.T) {
resetState(t)
body := strings.NewReader(`{"title": "Test", "status": "todo"}`)
resp, err := http.Post(baseURL+"/api/tasks", "application/json", body)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
t.Errorf("expected 201, got %d", resp.StatusCode)
}
var task map[string]interface{}
json.NewDecoder(resp.Body).Decode(&task)
if _, ok := task["id"]; !ok {
t.Error("expected id field")
}
}

For CI/CD environments:

version: '3.8'
services:
mockd:
image: ghcr.io/getmockd/mockd:latest
ports:
- "4280:4280"
- "4290:4290"
volumes:
- ./test-mocks.json:/mocks/config.json
command: start --config /mocks/config.json
app-tests:
build: .
depends_on:
- mockd
environment:
- API_BASE_URL=http://mockd:4280
command: npm test

.github/workflows/test.yml:

name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mockd
run: |
curl -sSL https://github.com/getmockd/mockd/releases/latest/download/mockd-linux-amd64 -o mockd
chmod +x mockd
sudo mv mockd /usr/local/bin/
- name: Start mockd
run: |
mockd start --config test-mocks.json &
sleep 2
- name: Run tests
run: npm test
env:
API_BASE_URL: http://localhost:4280
{
"stateful": {
"resources": {
"users": {
"seed": [
{"id": 1, "name": "Test User", "email": "test@example.com"}
]
}
}
}
}

Create multiple config files:

  • mocks-success.json - Happy path
  • mocks-errors.json - Error scenarios
  • mocks-slow.json - Timeout testing

Reset state in each test to ensure isolation:

beforeEach(async () => {
await fetch('http://localhost:4290/state', { method: 'DELETE' });
});

Add mocks at runtime for specific test scenarios:

test('handles rate limiting', async () => {
await fetch('http://localhost:4290/mocks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request: { method: 'GET', path: '/api/limited' },
response: {
status: 429,
headers: { 'Retry-After': '60' },
body: { error: 'Rate limited' }
}
})
});
// Test your rate limit handling
});