Split View: 모의 서버(Mock Server) 구축 완벽 가이드
모의 서버(Mock Server) 구축 완벽 가이드
개요
현대 소프트웨어 개발에서 프론트엔드와 백엔드는 대부분 별도 팀이 동시에 작업합니다. 백엔드 API가 완성될 때까지 프론트엔드 개발이 블로킹되거나, 외부 서드파티 API에 의존하는 기능을 테스트할 때 실제 서비스에 접근할 수 없는 상황이 자주 발생합니다.
**모의 서버(Mock Server)**는 이런 문제를 해결하는 핵심 도구입니다. API 계약을 먼저 정의하고, 실제 백엔드 없이도 프론트엔드 개발과 테스트를 진행할 수 있게 해줍니다.
1. 모의 서버가 필요한 이유
1.1 프론트엔드-백엔드 병렬 개발
전통적인 개발 방식 (순차):
백엔드 API 완성 → 프론트엔드 개발 시작
└── 지연: 프론트엔드가 백엔드를 기다림
모의 서버를 활용한 방식 (병렬):
API 스펙 정의 → 백엔드 개발 || 프론트엔드 개발
↑ 모의 서버로 블로킹 없음
두 팀이 API 계약(Contract)만 먼저 합의하면 동시에 개발할 수 있습니다.
1.2 외부 API 의존성 제거
외부 결제 API, 지도 서비스, 날씨 API 등 서드파티 서비스는 다음 문제가 있습니다:
- 비용 발생: 호출마다 과금되는 API
- 속도 제한: Rate Limiting으로 테스트 실행이 느려짐
- 서비스 불안정: 외부 서비스 장애로 CI/CD 파이프라인이 실패
- 테스트 데이터 오염: 실제 데이터에 테스트 데이터가 섞임
1.3 에러 케이스 재현
실제 서비스에서 재현하기 어려운 상황을 모의 서버로 쉽게 재현할 수 있습니다:
- 네트워크 타임아웃
- 500 Internal Server Error
- 429 Too Many Requests
- 네트워크 연결 끊김
- 응답 지연 (느린 네트워크 시뮬레이션)
1.4 테스트 환경 안정성
CI/CD 파이프라인에서 외부 의존성 없애기:
실제 DB 없이 → InMemory DB Fake
실제 API 없이 → Mock Server
실제 메시지 큐 없이 → In-process Fake
2. json-server (가장 빠른 시작)
json-server는 JSON 파일 하나로 30초 만에 REST API 서버를 만들 수 있는 도구입니다.
2.1 설치와 기본 사용법
# 전역 설치
npm install -g json-server
# 또는 프로젝트에 설치
npm install --save-dev json-server
2.2 db.json 구조 설계
{
"users": [
{ "id": 1, "name": "홍길동", "email": "hong@example.com", "role": "admin" },
{ "id": 2, "name": "이순신", "email": "lee@example.com", "role": "user" }
],
"posts": [
{ "id": 1, "title": "첫 번째 포스트", "body": "내용입니다", "userId": 1, "published": true },
{ "id": 2, "title": "두 번째 포스트", "body": "내용입니다", "userId": 2, "published": false }
],
"comments": [{ "id": 1, "text": "좋은 글이네요", "postId": 1, "userId": 2 }],
"profile": {
"name": "관리자",
"version": "1.0.0"
}
}
json-server 실행:
json-server --watch db.json --port 3001
자동으로 생성되는 엔드포인트:
GET /users
GET /users/1
POST /users
PUT /users/1
PATCH /users/1
DELETE /users/1
GET /posts?userId=1
GET /posts?published=true
GET /posts?_page=1&_limit=10
GET /posts?_sort=title&_order=asc
2.3 커스텀 라우팅 (routes.json)
{
"/api/*": "/$1",
"/api/v1/users/:id": "/users/:id",
"/blog/:id/comments": "/comments?postId=:id"
}
실행 시 라우팅 파일 적용:
json-server --watch db.json --routes routes.json --port 3001
2.4 미들웨어 추가
// middleware.js
module.exports = (req, res, next) => {
// 인증 헤더 검사
if (req.headers.authorization !== 'Bearer valid-token') {
if (req.method !== 'GET') {
return res.status(401).json({ error: 'Unauthorized' })
}
}
// 응답 지연 시뮬레이션 (500ms)
setTimeout(next, 500)
}
json-server --watch db.json --middlewares middleware.js
2.5 관계형 데이터 모델링
json-server는 _embed와 _expand를 통해 관계형 데이터를 지원합니다:
# 댓글 포함한 포스트 조회
GET /posts/1?_embed=comments
# 작성자 정보 포함
GET /posts?_expand=user
# 중첩 관계
GET /users?_embed=posts
2.6 페이지네이션, 정렬, 필터링
# 페이지네이션 (10개씩, 2페이지)
GET /posts?_page=2&_limit=10
# 정렬 (제목 내림차순)
GET /posts?_sort=title&_order=desc
# 필터링 (published=true인 포스트)
GET /posts?published=true
# 검색 (전체 텍스트)
GET /posts?q=검색어
# 범위 필터
GET /posts?id_gte=5&id_lte=10
2.7 커스텀 서버 스크립트
// server.js - 더 세밀한 제어가 필요할 때
const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('db.json')
const middlewares = jsonServer.defaults()
server.use(middlewares)
// 커스텀 라우트
server.post('/auth/login', (req, res) => {
const { email, password } = req.body
if (email === 'admin@example.com' && password === 'password') {
res.json({ token: 'fake-jwt-token', userId: 1 })
} else {
res.status(401).json({ error: 'Invalid credentials' })
}
})
server.use(router)
server.listen(3001, () => {
console.log('JSON Server is running on port 3001')
})
3. MSW (Mock Service Worker) - 현대적 방법
MSW는 Service Worker를 활용해 네트워크 레벨에서 요청을 가로채는 현대적인 API 모킹 라이브러리입니다. 브라우저와 Node.js 양쪽에서 동일한 핸들러를 사용할 수 있습니다.
3.1 설치 및 설정
npm install msw --save-dev
# Service Worker 파일 생성 (public 폴더에)
npx msw init public/ --save
3.2 REST API 핸들러 작성
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET 사용자 목록
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: '홍길동', email: 'hong@example.com' },
{ id: 2, name: '이순신', email: 'lee@example.com' },
])
}),
// GET 특정 사용자
http.get('/api/users/:id', ({ params }) => {
const { id } = params
if (id === '999') {
return new HttpResponse(null, { status: 404 })
}
return HttpResponse.json({
id: Number(id),
name: '홍길동',
email: 'hong@example.com',
})
}),
// POST 사용자 생성
http.post('/api/users', async ({ request }) => {
const newUser = await request.json()
return HttpResponse.json({ id: Date.now(), ...newUser }, { status: 201 })
}),
// PUT 사용자 수정
http.put('/api/users/:id', async ({ params, request }) => {
const { id } = params
const updates = await request.json()
return HttpResponse.json({ id: Number(id), ...updates })
}),
// DELETE 사용자 삭제
http.delete('/api/users/:id', ({ params }) => {
return new HttpResponse(null, { status: 204 })
}),
]
3.3 GraphQL 핸들러
import { graphql, HttpResponse } from 'msw'
export const graphqlHandlers = [
graphql.query('GetUser', ({ variables }) => {
const { id } = variables
return HttpResponse.json({
data: {
user: {
id,
name: '홍길동',
email: 'hong@example.com',
posts: [{ id: 1, title: '첫 번째 포스트' }],
},
},
})
}),
graphql.mutation('CreateUser', ({ variables }) => {
const { name, email } = variables
return HttpResponse.json({
data: {
createUser: {
id: String(Date.now()),
name,
email,
},
},
})
}),
]
3.4 동적 응답 (요청 파라미터 활용)
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/products', ({ request }) => {
const url = new URL(request.url)
const category = url.searchParams.get('category')
const page = Number(url.searchParams.get('page') ?? '1')
const limit = Number(url.searchParams.get('limit') ?? '10')
const allProducts = [
{ id: 1, name: '노트북', category: 'electronics', price: 1200000 },
{ id: 2, name: '마우스', category: 'electronics', price: 30000 },
{ id: 3, name: '책상', category: 'furniture', price: 250000 },
]
let filtered = allProducts
if (category) {
filtered = allProducts.filter((p) => p.category === category)
}
const start = (page - 1) * limit
const data = filtered.slice(start, start + limit)
return HttpResponse.json({
data,
total: filtered.length,
page,
limit,
})
}),
]
3.5 에러 응답 시나리오
import { http, HttpResponse } from 'msw'
// 에러 시나리오를 동적으로 제어하는 패턴
let shouldFailNext = false
export const handlers = [
// 에러 시뮬레이션 제어 엔드포인트 (테스트 전용)
http.post('/test/simulate-error', async ({ request }) => {
const { fail } = await request.json()
shouldFailNext = fail
return new HttpResponse(null, { status: 200 })
}),
http.post('/api/payment', async ({ request }) => {
if (shouldFailNext) {
shouldFailNext = false
return HttpResponse.json(
{ error: 'PAYMENT_DECLINED', message: '카드가 거절되었습니다' },
{ status: 402 }
)
}
const { amount } = await request.json()
return HttpResponse.json({
paymentId: `PAY-${Date.now()}`,
amount,
status: 'SUCCESS',
})
}),
// 네트워크 에러 시뮬레이션
http.get('/api/unstable', () => {
if (Math.random() > 0.7) {
return HttpResponse.error()
}
return HttpResponse.json({ data: 'success' })
}),
]
3.6 브라우저 환경 설정
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// src/main.tsx (React 앱 진입점)
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') {
return
}
const { worker } = await import('./mocks/browser')
return worker.start({
onUnhandledRequest: 'bypass' // 처리되지 않은 요청은 그대로 통과
})
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
})
3.7 Node.js 환경 설정 (테스트)
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// jest.setup.ts
import { server } from './src/mocks/server'
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// 테스트에서 핸들러 오버라이드
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
test('API 에러 처리 테스트', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Server Error' }, { status: 500 })
})
)
const result = await fetchUsers()
expect(result.error).toBe('Server Error')
})
3.8 Next.js에서 MSW 설정
// next.config.js
const nextConfig = {
// Service Worker를 위한 헤더 설정
async headers() {
return [
{
source: '/mockServiceWorker.js',
headers: [{ key: 'Service-Worker-Allowed', value: '/' }],
},
]
},
}
// src/app/providers.tsx (App Router)
'use client'
import { useEffect } from 'react'
export function MSWProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
import('../mocks/browser').then(({ worker }) => {
worker.start({ onUnhandledRequest: 'bypass' })
})
}
}, [])
return children
}
4. WireMock - Java/JVM 환경
WireMock은 Java/JVM 생태계에서 가장 널리 사용되는 HTTP 모의 서버입니다.
4.1 WireMock Standalone 서버 실행
# WireMock 다운로드
curl -o wiremock.jar https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.35.0/wiremock-jre8-standalone-2.35.0.jar
# 실행
java -jar wiremock.jar --port 8080 --verbose
디렉토리 구조:
wiremock/
├── mappings/ # Stub 정의 JSON 파일
│ ├── users.json
│ └── orders.json
└── __files/ # 응답 파일 (body 파일)
├── users.json
└── order-template.json
4.2 JSON 파일로 Stub 정의
{
"request": {
"method": "GET",
"url": "/api/users"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": [
{ "id": 1, "name": "홍길동", "email": "hong@example.com" },
{ "id": 2, "name": "이순신", "email": "lee@example.com" }
]
}
}
4.3 Request Matching (URL, headers, body)
{
"request": {
"method": "POST",
"url": "/api/orders",
"headers": {
"Content-Type": {
"equalTo": "application/json"
},
"Authorization": {
"matches": "Bearer .+"
}
},
"bodyPatterns": [
{
"matchesJsonPath": "$.items[*].productId"
},
{
"equalToJson": {
"userId": 1
},
"ignoreArrayOrder": true,
"ignoreExtraElements": true
}
]
},
"response": {
"status": 201,
"jsonBody": {
"orderId": "ORDER-001",
"status": "PENDING"
}
}
}
4.4 Response Templating (Handlebars)
{
"request": {
"method": "GET",
"urlPathPattern": "/api/users/([0-9]+)"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{ \"id\": {{request.pathSegments.[2]}}, \"name\": \"사용자-{{request.pathSegments.[2]}}\", \"timestamp\": \"{{now}}\" }",
"transformers": ["response-template"]
}
}
4.5 Scenarios (상태 기반 모킹)
시나리오를 사용하면 상태에 따라 다른 응답을 반환할 수 있습니다:
{
"scenarioName": "주문 처리 흐름",
"requiredScenarioState": "Started",
"newScenarioState": "결제 완료",
"request": {
"method": "POST",
"url": "/api/orders/pay"
},
"response": {
"status": 200,
"jsonBody": { "status": "PAID" }
}
}
{
"scenarioName": "주문 처리 흐름",
"requiredScenarioState": "결제 완료",
"newScenarioState": "배송 중",
"request": {
"method": "GET",
"url": "/api/orders/ORDER-001"
},
"response": {
"status": 200,
"jsonBody": { "status": "SHIPPING" }
}
}
4.6 Record and Playback
실제 API 응답을 기록하고 재생하는 기능:
# 기록 모드로 실행 (실제 API로 프록시)
java -jar wiremock.jar --proxy-all="https://api.example.com" --record-mappings --port 8080
# 기록된 응답 재생 (기본 모드)
java -jar wiremock.jar --port 8080
5. Prism - OpenAPI 기반 모의 서버
Prism은 OpenAPI(Swagger) 스펙에서 자동으로 모의 서버를 생성하는 도구입니다.
5.1 설치와 실행
npm install -g @stoplight/prism-cli
# OpenAPI 스펙으로 모의 서버 실행
npx @stoplight/prism-cli mock api.yaml --port 4010
5.2 OpenAPI 스펙 예시
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
operationId: getUsers
responses:
'200':
description: 사용자 목록
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
examples:
default:
value:
- id: 1
name: 홍길동
email: hong@example.com
post:
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: 사용자 생성됨
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
required: [id, name, email]
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
CreateUserRequest:
type: object
required: [name, email]
properties:
name:
type: string
email:
type: string
format: email
5.3 동적 응답 생성
Prism은 스펙에서 예시값이 없으면 스키마를 기반으로 자동으로 값을 생성합니다:
# 동적 생성 모드 (스키마 기반 랜덤 응답)
npx @stoplight/prism-cli mock api.yaml --dynamic
5.4 Validation 모드
# Validation 모드 - 요청/응답을 스펙과 대조 검증
npx @stoplight/prism-cli proxy api.yaml https://api.example.com --validate-request --validate-response
6. Go로 모의 서버 구현
Go 표준 라이브러리만으로 완전한 모의 서버를 구현할 수 있습니다.
6.1 기본 모의 서버 구조
package mockserver
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
)
type MockServer struct {
server *http.Server
mux *http.ServeMux
mu sync.RWMutex
stubs map[string]*Stub
calls []RequestLog
}
type Stub struct {
Method string
Path string
StatusCode int
Body interface{}
Headers map[string]string
Delay time.Duration
}
type RequestLog struct {
Method string
Path string
Headers map[string][]string
Body []byte
Timestamp time.Time
}
func NewMockServer(port string) *MockServer {
ms := &MockServer{
mux: http.NewServeMux(),
stubs: make(map[string]*Stub),
}
ms.server = &http.Server{
Addr: ":" + port,
Handler: ms.mux,
}
ms.mux.HandleFunc("/", ms.handleRequest)
// 테스트 제어 엔드포인트
ms.mux.HandleFunc("/__admin/stubs", ms.handleAdminStubs)
ms.mux.HandleFunc("/__admin/calls", ms.handleAdminCalls)
ms.mux.HandleFunc("/__admin/reset", ms.handleAdminReset)
return ms
}
func (ms *MockServer) Start() {
go func() {
log.Printf("MockServer starting on %s", ms.server.Addr)
if err := ms.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("MockServer failed: %v", err)
}
}()
// 서버 시작 대기
time.Sleep(100 * time.Millisecond)
}
func (ms *MockServer) Stop() {
ms.server.Close()
}
func (ms *MockServer) AddStub(stub *Stub) {
ms.mu.Lock()
defer ms.mu.Unlock()
key := stub.Method + ":" + stub.Path
ms.stubs[key] = stub
}
func (ms *MockServer) handleRequest(w http.ResponseWriter, r *http.Request) {
// 요청 로깅
ms.mu.Lock()
ms.calls = append(ms.calls, RequestLog{
Method: r.Method,
Path: r.URL.Path,
Headers: r.Header,
Timestamp: time.Now(),
})
ms.mu.Unlock()
// Stub 조회
ms.mu.RLock()
key := r.Method + ":" + r.URL.Path
stub, ok := ms.stubs[key]
ms.mu.RUnlock()
if !ok {
http.Error(w, "No stub found for "+r.Method+" "+r.URL.Path, http.StatusNotFound)
return
}
// 응답 지연 시뮬레이션
if stub.Delay > 0 {
time.Sleep(stub.Delay)
}
// 헤더 설정
for k, v := range stub.Headers {
w.Header().Set(k, v)
}
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(stub.StatusCode)
if stub.Body != nil {
json.NewEncoder(w).Encode(stub.Body)
}
}
func (ms *MockServer) handleAdminReset(w http.ResponseWriter, r *http.Request) {
ms.mu.Lock()
ms.stubs = make(map[string]*Stub)
ms.calls = nil
ms.mu.Unlock()
w.WriteHeader(http.StatusOK)
}
func (ms *MockServer) handleAdminCalls(w http.ResponseWriter, r *http.Request) {
ms.mu.RLock()
defer ms.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ms.calls)
}
func (ms *MockServer) handleAdminStubs(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
var stub Stub
if err := json.NewDecoder(r.Body).Decode(&stub); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ms.AddStub(&stub)
w.WriteHeader(http.StatusCreated)
return
}
ms.mu.RLock()
defer ms.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ms.stubs)
}
6.2 테스트에서 모의 서버 사용
func TestUserService_GetUser(t *testing.T) {
// 모의 서버 시작
ms := NewMockServer("9999")
ms.Start()
defer ms.Stop()
// Stub 설정
ms.AddStub(&Stub{
Method: "GET",
Path: "/api/users/1",
StatusCode: 200,
Body: map[string]interface{}{
"id": 1,
"name": "홍길동",
"email": "hong@example.com",
},
})
// 테스트 대상 서비스 (모의 서버 URL 주입)
client := NewUserServiceClient("http://localhost:9999")
user, err := client.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "홍길동" {
t.Errorf("expected 홍길동, got %s", user.Name)
}
}
func TestUserService_GetUser_NotFound(t *testing.T) {
ms := NewMockServer("9998")
ms.Start()
defer ms.Stop()
ms.AddStub(&Stub{
Method: "GET",
Path: "/api/users/999",
StatusCode: 404,
Body: map[string]string{"error": "User not found"},
})
client := NewUserServiceClient("http://localhost:9998")
_, err := client.GetUser(999)
if err == nil {
t.Error("expected error, got nil")
}
}
7. TypeScript/Node.js로 모의 서버 구현
7.1 Express.js + TypeScript 모의 서버
// mock-server/index.ts
import express, { Request, Response, NextFunction } from 'express'
import fs from 'fs'
import path from 'path'
interface StubDefinition {
method: string
path: string
statusCode: number
body?: unknown
headers?: Record<string, string>
delay?: number
}
const app = express()
app.use(express.json())
const stubs: Map<string, StubDefinition> = new Map()
const requestLog: Array<{ method: string; path: string; timestamp: string }> = []
// 요청 로깅 미들웨어
app.use((req: Request, res: Response, next: NextFunction) => {
requestLog.push({
method: req.method,
path: req.path,
timestamp: new Date().toISOString(),
})
next()
})
// 관리 API
app.post('/__admin/stubs', (req: Request, res: Response) => {
const stub: StubDefinition = req.body
const key = `${stub.method}:${stub.path}`
stubs.set(key, stub)
res.status(201).json({ message: 'Stub registered', key })
})
app.delete('/__admin/stubs', (req: Request, res: Response) => {
stubs.clear()
requestLog.length = 0
res.status(200).json({ message: 'All stubs cleared' })
})
app.get('/__admin/calls', (req: Request, res: Response) => {
res.json(requestLog)
})
// 동적 응답 핸들러
app.use(async (req: Request, res: Response) => {
const key = `${req.method}:${req.path}`
const stub = stubs.get(key)
if (!stub) {
return res.status(404).json({
error: 'No stub found',
request: { method: req.method, path: req.path },
})
}
// 응답 지연
if (stub.delay && stub.delay > 0) {
await new Promise((resolve) => setTimeout(resolve, stub.delay))
}
// 헤더 설정
if (stub.headers) {
Object.entries(stub.headers).forEach(([key, value]) => {
res.setHeader(key, value)
})
}
res.status(stub.statusCode).json(stub.body)
})
const PORT = process.env.MOCK_PORT ?? '3001'
app.listen(PORT, () => {
console.log(`Mock server running on port ${PORT}`)
})
export { app }
7.2 파일 기반 응답 라우팅
// 응답 파일을 디렉토리 구조로 관리
// responses/
// GET/
// api/
// users/
// index.json -> GET /api/users
// 1.json -> GET /api/users/1
// POST/
// api/
// users/
// index.json -> POST /api/users (응답 템플릿)
app.use((req: Request, res: Response) => {
const responsePath = path.join(__dirname, 'responses', req.method, req.path, 'index.json')
if (fs.existsSync(responsePath)) {
const responseData = JSON.parse(fs.readFileSync(responsePath, 'utf-8'))
res.json(responseData)
} else {
res.status(404).json({ error: 'No response file found' })
}
})
7.3 미들웨어로 지연, 에러 주입
// chaos-middleware.ts - 혼돈 엔지니어링을 위한 미들웨어
interface ChaosConfig {
errorRate: number // 0.0 ~ 1.0
minDelay: number // ms
maxDelay: number // ms
}
let chaosConfig: ChaosConfig = {
errorRate: 0,
minDelay: 0,
maxDelay: 0,
}
export const chaosMiddleware = async (req: Request, res: Response, next: NextFunction) => {
// 응답 지연
const delay = chaosConfig.minDelay + Math.random() * (chaosConfig.maxDelay - chaosConfig.minDelay)
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
// 무작위 에러 주입
if (Math.random() < chaosConfig.errorRate) {
return res.status(500).json({
error: 'Chaos monkey strikes!',
timestamp: new Date().toISOString(),
})
}
next()
}
// 설정 변경 API
app.post('/__admin/chaos', (req: Request, res: Response) => {
chaosConfig = { ...chaosConfig, ...req.body }
res.json({ message: 'Chaos config updated', config: chaosConfig })
})
8. Docker Compose로 모의 서버 환경 구성
8.1 docker-compose.yml 예시
version: '3.8'
services:
# json-server 모의 서버
json-server:
image: clue/json-server
volumes:
- ./mock-data/db.json:/data/db.json
- ./mock-data/routes.json:/data/routes.json
ports:
- '3001:80'
command: --watch db.json --routes routes.json
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost/users']
interval: 10s
timeout: 5s
retries: 3
# WireMock 서버
wiremock:
image: wiremock/wiremock:latest
ports:
- '8080:8080'
volumes:
- ./wiremock/mappings:/home/wiremock/mappings
- ./wiremock/__files:/home/wiremock/__files
command:
- '--verbose'
- '--global-response-templating'
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/__admin/health']
interval: 10s
timeout: 5s
retries: 3
# 실제 애플리케이션 (모의 서버를 의존성으로 사용)
app:
build: .
ports:
- '8000:8000'
environment:
- USER_SERVICE_URL=http://json-server:80
- PAYMENT_SERVICE_URL=http://wiremock:8080
depends_on:
json-server:
condition: service_healthy
wiremock:
condition: service_healthy
# E2E 테스트 실행
e2e-tests:
build:
context: .
dockerfile: Dockerfile.test
environment:
- APP_URL=http://app:8000
depends_on:
- app
command: npm run test:e2e
profiles:
- test
8.2 테스트 실행 스크립트
#!/bin/bash
# run-tests.sh
echo "모의 서버 환경 시작..."
docker-compose up -d json-server wiremock
echo "헬스체크 대기..."
docker-compose exec json-server wget -qO- http://localhost/users
docker-compose exec wiremock wget -qO- http://localhost:8080/__admin/health
echo "애플리케이션 시작..."
docker-compose up -d app
echo "E2E 테스트 실행..."
docker-compose run --rm e2e-tests
echo "정리..."
docker-compose down
8.3 모의 데이터 초기화 스크립트
// scripts/setup-mock-data.ts
const WIREMOCK_URL = 'http://localhost:8080/__admin/mappings'
const stubs = [
{
request: {
method: 'POST',
url: '/api/payment/charge',
},
response: {
status: 200,
headers: { 'Content-Type': 'application/json' },
jsonBody: {
paymentId: 'TEST-PAYMENT-001',
status: 'SUCCESS',
amount: 10000,
},
},
},
{
request: {
method: 'POST',
url: '/api/payment/charge',
headers: {
'X-Simulate-Failure': { equalTo: 'true' },
},
},
response: {
status: 402,
headers: { 'Content-Type': 'application/json' },
jsonBody: {
error: 'PAYMENT_DECLINED',
message: '카드가 거절되었습니다',
},
},
},
]
async function setupMockData() {
for (const stub of stubs) {
const response = await fetch(WIREMOCK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(stub),
})
console.log(`Stub registered: ${response.status}`)
}
}
setupMockData().catch(console.error)
9. 퀴즈
퀴즈 1: MSW(Mock Service Worker)가 다른 모킹 방법과 다른 핵심적인 차이점은 무엇인가요?
정답: MSW는 Service Worker를 활용해 네트워크 레벨에서 요청을 가로채기 때문에, 애플리케이션 코드를 수정하지 않고도 실제 네트워크 요청을 인터셉트할 수 있습니다. 또한 브라우저와 Node.js 양쪽에서 동일한 핸들러를 재사용할 수 있습니다.
설명: axios-mock-adapter나 fetch-mock 같은 라이브러리는 특정 HTTP 클라이언트 라이브러리를 모킹하므로, HTTP 클라이언트가 바뀌면 모킹 코드도 변경해야 합니다. MSW는 실제 HTTP 스택을 통해 요청이 발생하므로 어떤 HTTP 라이브러리를 사용해도 동일하게 동작합니다.
퀴즈 2: json-server의 가장 큰 제약 사항은 무엇이고, 어떻게 극복할 수 있나요?
정답: json-server의 가장 큰 제약 사항은 복잡한 비즈니스 로직 구현이 어렵다는 점입니다. 단순 CRUD 외의 커스텀 응답 로직이 필요할 때 한계가 있습니다.
설명: 이를 극복하려면 json-server를 라이브러리로 사용하여 커스텀 라우터를 추가(server.post('/auth/login', ...))하거나, 미들웨어를 작성하여 복잡한 로직을 처리할 수 있습니다. 더 복잡한 시나리오가 필요하다면 WireMock이나 직접 구현한 모의 서버로 전환하는 것이 좋습니다.
퀴즈 3: WireMock의 Scenarios 기능은 어떤 상황에서 유용한가요?
정답: Scenarios는 상태에 따라 다른 응답을 반환해야 하는 워크플로우 테스트에 유용합니다. 예를 들어 주문 상태(결제 대기 → 결제 완료 → 배송 중 → 배송 완료)나 재시도 로직(첫 번째 요청은 실패, 두 번째는 성공) 테스트에 적합합니다.
설명: 일반적인 Stub은 항상 동일한 응답을 반환하지만, Scenario를 사용하면 요청이 들어올 때마다 상태가 전이되며 각 상태에 맞는 응답을 반환합니다. 이는 멱등성이 없는 API나 상태 기반 워크플로우를 테스트할 때 필수적입니다.
퀴즈 4: Prism을 사용할 때의 주요 장점은 무엇인가요?
정답: Prism의 주요 장점은 OpenAPI 스펙이 있으면 별도의 모킹 코드 없이 자동으로 모의 서버를 생성한다는 점입니다. 또한 Validation 모드에서 실제 API 요청/응답을 스펙과 대조하여 계약 준수 여부를 자동으로 검증할 수 있습니다.
설명: API 스펙 우선 개발(API-First Design) 방식에서 특히 유용합니다. 백엔드 구현 전에 API 스펙을 먼저 정의하면, 프론트엔드는 Prism으로 즉시 개발을 시작할 수 있고, 백엔드가 완성된 후에는 Validation 모드로 스펙 준수 여부를 검증할 수 있습니다.
퀴즈 5: 모의 서버를 사용할 때 "계약 테스트(Contract Testing)"와 결합해야 하는 이유는 무엇인가요?
정답: 모의 서버는 실제 API와 동기화되지 않을 위험이 있습니다. 계약 테스트를 결합하면 모의 서버가 실제 API와 일치하는지 자동으로 검증할 수 있어 "모의 서버와 실제 API가 달라서 발생하는 버그"를 방지할 수 있습니다.
설명: 모의 서버를 사용해 개발했는데 실제 API가 변경되면, 개발 환경에서는 테스트가 통과하지만 실제 환경에서는 실패하는 문제가 발생할 수 있습니다. Pact와 같은 계약 테스트 도구를 사용하면 Consumer(프론트엔드)가 기대하는 API 계약을 Provider(백엔드)가 준수하는지 자동으로 검증할 수 있습니다.
Mock Server Building Complete Guide
Overview
In modern software development, frontend and backend teams almost always work in parallel. Waiting for backend APIs to be ready before starting frontend work, or being unable to access third-party services during testing, is a constant friction point.
A Mock Server is the key tool for solving these problems. By defining the API contract first, you can develop and test the frontend without a real backend.
1. Why You Need a Mock Server
1.1 Parallel Frontend-Backend Development
Traditional approach (sequential):
Backend API complete → Frontend development begins
└── Delay: frontend blocked waiting for backend
With a mock server (parallel):
API spec defined → Backend dev || Frontend dev
↑ No blocking via mock server
Both teams can work simultaneously as long as they agree on the API contract first.
1.2 Removing External API Dependencies
Third-party services like payment APIs, mapping services, or weather APIs come with problems:
- Costs: APIs that charge per call
- Rate Limiting: Slows down test execution
- Service Instability: External outages break CI/CD pipelines
- Test Data Pollution: Test data mixing into production data
1.3 Reproducing Error Scenarios
A mock server makes it easy to reproduce situations that are hard to trigger in real services:
- Network timeouts
- 500 Internal Server Error
- 429 Too Many Requests
- Network disconnection
- Response delays (slow network simulation)
1.4 Test Environment Stability
Eliminating external dependencies in CI/CD:
No real DB → InMemory DB Fake
No real API → Mock Server
No real queue → In-process Fake
2. json-server (Fastest Start)
json-server lets you create a full REST API server from a single JSON file in under 30 seconds.
2.1 Installation and Basic Usage
# Global install
npm install -g json-server
# Or as a dev dependency
npm install --save-dev json-server
2.2 db.json Structure
{
"users": [
{ "id": 1, "name": "Alice", "email": "alice@example.com", "role": "admin" },
{ "id": 2, "name": "Bob", "email": "bob@example.com", "role": "user" }
],
"posts": [
{ "id": 1, "title": "First Post", "body": "Content here", "userId": 1, "published": true },
{ "id": 2, "title": "Second Post", "body": "Content here", "userId": 2, "published": false }
],
"comments": [{ "id": 1, "text": "Great post!", "postId": 1, "userId": 2 }],
"profile": {
"name": "Admin",
"version": "1.0.0"
}
}
Start json-server:
json-server --watch db.json --port 3001
Auto-generated endpoints:
GET /users
GET /users/1
POST /users
PUT /users/1
PATCH /users/1
DELETE /users/1
GET /posts?userId=1
GET /posts?published=true
GET /posts?_page=1&_limit=10
GET /posts?_sort=title&_order=asc
2.3 Custom Routing (routes.json)
{
"/api/*": "/$1",
"/api/v1/users/:id": "/users/:id",
"/blog/:id/comments": "/comments?postId=:id"
}
Apply routing on startup:
json-server --watch db.json --routes routes.json --port 3001
2.4 Adding Middleware
// middleware.js
module.exports = (req, res, next) => {
// Check auth header
if (req.headers.authorization !== 'Bearer valid-token') {
if (req.method !== 'GET') {
return res.status(401).json({ error: 'Unauthorized' })
}
}
// Simulate response delay (500ms)
setTimeout(next, 500)
}
json-server --watch db.json --middlewares middleware.js
2.5 Relational Data Modeling
json-server supports relational data via _embed and _expand:
# Get post with embedded comments
GET /posts/1?_embed=comments
# Get post with expanded user info
GET /posts?_expand=user
# Nested relationships
GET /users?_embed=posts
2.6 Pagination, Sorting, and Filtering
# Pagination (10 per page, page 2)
GET /posts?_page=2&_limit=10
# Sort by title descending
GET /posts?_sort=title&_order=desc
# Filter published posts
GET /posts?published=true
# Full-text search
GET /posts?q=search-term
# Range filter
GET /posts?id_gte=5&id_lte=10
2.7 Custom Server Script
// server.js - for finer control
const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('db.json')
const middlewares = jsonServer.defaults()
server.use(middlewares)
// Custom routes
server.post('/auth/login', (req, res) => {
const { email, password } = req.body
if (email === 'admin@example.com' && password === 'password') {
res.json({ token: 'fake-jwt-token', userId: 1 })
} else {
res.status(401).json({ error: 'Invalid credentials' })
}
})
server.use(router)
server.listen(3001, () => {
console.log('JSON Server is running on port 3001')
})
3. MSW (Mock Service Worker) - The Modern Approach
MSW uses Service Workers to intercept requests at the network level. It lets you reuse the same handlers in both the browser and Node.js.
3.1 Installation and Setup
npm install msw --save-dev
# Generate Service Worker file in the public directory
npx msw init public/ --save
3.2 Writing REST API Handlers
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET list of users
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
])
}),
// GET specific user
http.get('/api/users/:id', ({ params }) => {
const { id } = params
if (id === '999') {
return new HttpResponse(null, { status: 404 })
}
return HttpResponse.json({
id: Number(id),
name: 'Alice',
email: 'alice@example.com',
})
}),
// POST create user
http.post('/api/users', async ({ request }) => {
const newUser = await request.json()
return HttpResponse.json({ id: Date.now(), ...newUser }, { status: 201 })
}),
// PUT update user
http.put('/api/users/:id', async ({ params, request }) => {
const { id } = params
const updates = await request.json()
return HttpResponse.json({ id: Number(id), ...updates })
}),
// DELETE user
http.delete('/api/users/:id', ({ params }) => {
return new HttpResponse(null, { status: 204 })
}),
]
3.3 GraphQL Handlers
import { graphql, HttpResponse } from 'msw'
export const graphqlHandlers = [
graphql.query('GetUser', ({ variables }) => {
const { id } = variables
return HttpResponse.json({
data: {
user: {
id,
name: 'Alice',
email: 'alice@example.com',
posts: [{ id: 1, title: 'First Post' }],
},
},
})
}),
graphql.mutation('CreateUser', ({ variables }) => {
const { name, email } = variables
return HttpResponse.json({
data: {
createUser: {
id: String(Date.now()),
name,
email,
},
},
})
}),
]
3.4 Dynamic Responses Using Request Parameters
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/products', ({ request }) => {
const url = new URL(request.url)
const category = url.searchParams.get('category')
const page = Number(url.searchParams.get('page') ?? '1')
const limit = Number(url.searchParams.get('limit') ?? '10')
const allProducts = [
{ id: 1, name: 'Laptop', category: 'electronics', price: 1200 },
{ id: 2, name: 'Mouse', category: 'electronics', price: 30 },
{ id: 3, name: 'Desk', category: 'furniture', price: 250 },
]
let filtered = allProducts
if (category) {
filtered = allProducts.filter((p) => p.category === category)
}
const start = (page - 1) * limit
const data = filtered.slice(start, start + limit)
return HttpResponse.json({
data,
total: filtered.length,
page,
limit,
})
}),
]
3.5 Error Response Scenarios
import { http, HttpResponse } from 'msw'
// Pattern to dynamically control error scenarios
let shouldFailNext = false
export const handlers = [
// Test-only control endpoint
http.post('/test/simulate-error', async ({ request }) => {
const { fail } = await request.json()
shouldFailNext = fail
return new HttpResponse(null, { status: 200 })
}),
http.post('/api/payment', async ({ request }) => {
if (shouldFailNext) {
shouldFailNext = false
return HttpResponse.json(
{ error: 'PAYMENT_DECLINED', message: 'Card was declined' },
{ status: 402 }
)
}
const { amount } = await request.json()
return HttpResponse.json({
paymentId: `PAY-${Date.now()}`,
amount,
status: 'SUCCESS',
})
}),
// Network error simulation
http.get('/api/unstable', () => {
if (Math.random() > 0.7) {
return HttpResponse.error()
}
return HttpResponse.json({ data: 'success' })
}),
]
3.6 Browser Environment Setup
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// src/main.tsx (React entry point)
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') {
return
}
const { worker } = await import('./mocks/browser')
return worker.start({
onUnhandledRequest: 'bypass' // Pass through unhandled requests
})
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
})
3.7 Node.js Environment Setup (for Tests)
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// jest.setup.ts
import { server } from './src/mocks/server'
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// Override handlers in specific tests
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
test('handles API error gracefully', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Server Error' }, { status: 500 })
})
)
const result = await fetchUsers()
expect(result.error).toBe('Server Error')
})
3.8 MSW in Next.js
// next.config.js
const nextConfig = {
async headers() {
return [
{
source: '/mockServiceWorker.js',
headers: [{ key: 'Service-Worker-Allowed', value: '/' }],
},
]
},
}
// src/app/providers.tsx (App Router)
'use client'
import { useEffect } from 'react'
export function MSWProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
import('../mocks/browser').then(({ worker }) => {
worker.start({ onUnhandledRequest: 'bypass' })
})
}
}, [])
return children
}
4. WireMock - Java/JVM Environment
WireMock is the most widely used HTTP mock server in the Java/JVM ecosystem.
4.1 Running WireMock Standalone
# Download WireMock
curl -o wiremock.jar https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.35.0/wiremock-jre8-standalone-2.35.0.jar
# Run
java -jar wiremock.jar --port 8080 --verbose
Directory structure:
wiremock/
├── mappings/ # Stub definition JSON files
│ ├── users.json
│ └── orders.json
└── __files/ # Response body files
├── users.json
└── order-template.json
4.2 Defining Stubs with JSON Files
{
"request": {
"method": "GET",
"url": "/api/users"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
]
}
}
4.3 Request Matching (URL, Headers, Body)
{
"request": {
"method": "POST",
"url": "/api/orders",
"headers": {
"Content-Type": {
"equalTo": "application/json"
},
"Authorization": {
"matches": "Bearer .+"
}
},
"bodyPatterns": [
{
"matchesJsonPath": "$.items[*].productId"
},
{
"equalToJson": {
"userId": 1
},
"ignoreArrayOrder": true,
"ignoreExtraElements": true
}
]
},
"response": {
"status": 201,
"jsonBody": {
"orderId": "ORDER-001",
"status": "PENDING"
}
}
}
4.4 Response Templating (Handlebars)
{
"request": {
"method": "GET",
"urlPathPattern": "/api/users/([0-9]+)"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{ \"id\": {{request.pathSegments.[2]}}, \"name\": \"User-{{request.pathSegments.[2]}}\", \"timestamp\": \"{{now}}\" }",
"transformers": ["response-template"]
}
}
4.5 Scenarios (State-Based Mocking)
Scenarios let you return different responses based on current state:
{
"scenarioName": "Order Processing Flow",
"requiredScenarioState": "Started",
"newScenarioState": "Payment Complete",
"request": {
"method": "POST",
"url": "/api/orders/pay"
},
"response": {
"status": 200,
"jsonBody": { "status": "PAID" }
}
}
{
"scenarioName": "Order Processing Flow",
"requiredScenarioState": "Payment Complete",
"newScenarioState": "Shipping",
"request": {
"method": "GET",
"url": "/api/orders/ORDER-001"
},
"response": {
"status": 200,
"jsonBody": { "status": "SHIPPING" }
}
}
4.6 Record and Playback
Record real API responses and replay them:
# Run in record mode (proxy to real API)
java -jar wiremock.jar --proxy-all="https://api.example.com" --record-mappings --port 8080
# Replay recorded responses (default mode)
java -jar wiremock.jar --port 8080
5. Prism - OpenAPI-Based Mock Server
Prism automatically generates a mock server from an OpenAPI (Swagger) specification.
5.1 Installation and Usage
npm install -g @stoplight/prism-cli
# Run a mock server from an OpenAPI spec
npx @stoplight/prism-cli mock api.yaml --port 4010
5.2 OpenAPI Spec Example
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
operationId: getUsers
responses:
'200':
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
examples:
default:
value:
- id: 1
name: Alice
email: alice@example.com
post:
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
required: [id, name, email]
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
CreateUserRequest:
type: object
required: [name, email]
properties:
name:
type: string
email:
type: string
format: email
5.3 Dynamic Response Generation
Prism generates values automatically from schemas when no example is provided:
# Dynamic mode (schema-based random responses)
npx @stoplight/prism-cli mock api.yaml --dynamic
5.4 Validation Mode
# Validation mode - validates requests and responses against the spec
npx @stoplight/prism-cli proxy api.yaml https://api.example.com --validate-request --validate-response
6. Building a Mock Server in Go
The Go standard library is sufficient to build a complete mock server.
6.1 Core Mock Server Structure
package mockserver
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
)
type MockServer struct {
server *http.Server
mux *http.ServeMux
mu sync.RWMutex
stubs map[string]*Stub
calls []RequestLog
}
type Stub struct {
Method string
Path string
StatusCode int
Body interface{}
Headers map[string]string
Delay time.Duration
}
type RequestLog struct {
Method string
Path string
Headers map[string][]string
Body []byte
Timestamp time.Time
}
func NewMockServer(port string) *MockServer {
ms := &MockServer{
mux: http.NewServeMux(),
stubs: make(map[string]*Stub),
}
ms.server = &http.Server{
Addr: ":" + port,
Handler: ms.mux,
}
ms.mux.HandleFunc("/", ms.handleRequest)
// Admin control endpoints
ms.mux.HandleFunc("/__admin/stubs", ms.handleAdminStubs)
ms.mux.HandleFunc("/__admin/calls", ms.handleAdminCalls)
ms.mux.HandleFunc("/__admin/reset", ms.handleAdminReset)
return ms
}
func (ms *MockServer) Start() {
go func() {
log.Printf("MockServer starting on %s", ms.server.Addr)
if err := ms.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("MockServer failed: %v", err)
}
}()
time.Sleep(100 * time.Millisecond)
}
func (ms *MockServer) Stop() {
ms.server.Close()
}
func (ms *MockServer) AddStub(stub *Stub) {
ms.mu.Lock()
defer ms.mu.Unlock()
key := stub.Method + ":" + stub.Path
ms.stubs[key] = stub
}
func (ms *MockServer) handleRequest(w http.ResponseWriter, r *http.Request) {
// Log the request
ms.mu.Lock()
ms.calls = append(ms.calls, RequestLog{
Method: r.Method,
Path: r.URL.Path,
Headers: r.Header,
Timestamp: time.Now(),
})
ms.mu.Unlock()
// Look up stub
ms.mu.RLock()
key := r.Method + ":" + r.URL.Path
stub, ok := ms.stubs[key]
ms.mu.RUnlock()
if !ok {
http.Error(w, "No stub found for "+r.Method+" "+r.URL.Path, http.StatusNotFound)
return
}
// Simulate response delay
if stub.Delay > 0 {
time.Sleep(stub.Delay)
}
// Set headers
for k, v := range stub.Headers {
w.Header().Set(k, v)
}
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(stub.StatusCode)
if stub.Body != nil {
json.NewEncoder(w).Encode(stub.Body)
}
}
func (ms *MockServer) handleAdminReset(w http.ResponseWriter, r *http.Request) {
ms.mu.Lock()
ms.stubs = make(map[string]*Stub)
ms.calls = nil
ms.mu.Unlock()
w.WriteHeader(http.StatusOK)
}
func (ms *MockServer) handleAdminCalls(w http.ResponseWriter, r *http.Request) {
ms.mu.RLock()
defer ms.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ms.calls)
}
func (ms *MockServer) handleAdminStubs(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
var stub Stub
if err := json.NewDecoder(r.Body).Decode(&stub); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ms.AddStub(&stub)
w.WriteHeader(http.StatusCreated)
return
}
ms.mu.RLock()
defer ms.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ms.stubs)
}
6.2 Using the Mock Server in Tests
func TestUserService_GetUser(t *testing.T) {
ms := NewMockServer("9999")
ms.Start()
defer ms.Stop()
ms.AddStub(&Stub{
Method: "GET",
Path: "/api/users/1",
StatusCode: 200,
Body: map[string]interface{}{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
},
})
client := NewUserServiceClient("http://localhost:9999")
user, err := client.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("expected Alice, got %s", user.Name)
}
}
func TestUserService_GetUser_NotFound(t *testing.T) {
ms := NewMockServer("9998")
ms.Start()
defer ms.Stop()
ms.AddStub(&Stub{
Method: "GET",
Path: "/api/users/999",
StatusCode: 404,
Body: map[string]string{"error": "User not found"},
})
client := NewUserServiceClient("http://localhost:9998")
_, err := client.GetUser(999)
if err == nil {
t.Error("expected error, got nil")
}
}
7. Building a Mock Server in TypeScript/Node.js
7.1 Express.js + TypeScript Mock Server
// mock-server/index.ts
import express, { Request, Response, NextFunction } from 'express'
import fs from 'fs'
import path from 'path'
interface StubDefinition {
method: string
path: string
statusCode: number
body?: unknown
headers?: Record<string, string>
delay?: number
}
const app = express()
app.use(express.json())
const stubs: Map<string, StubDefinition> = new Map()
const requestLog: Array<{ method: string; path: string; timestamp: string }> = []
// Request logging middleware
app.use((req: Request, res: Response, next: NextFunction) => {
requestLog.push({
method: req.method,
path: req.path,
timestamp: new Date().toISOString(),
})
next()
})
// Admin API
app.post('/__admin/stubs', (req: Request, res: Response) => {
const stub: StubDefinition = req.body
const key = `${stub.method}:${stub.path}`
stubs.set(key, stub)
res.status(201).json({ message: 'Stub registered', key })
})
app.delete('/__admin/stubs', (req: Request, res: Response) => {
stubs.clear()
requestLog.length = 0
res.status(200).json({ message: 'All stubs cleared' })
})
app.get('/__admin/calls', (req: Request, res: Response) => {
res.json(requestLog)
})
// Dynamic response handler
app.use(async (req: Request, res: Response) => {
const key = `${req.method}:${req.path}`
const stub = stubs.get(key)
if (!stub) {
return res.status(404).json({
error: 'No stub found',
request: { method: req.method, path: req.path },
})
}
// Response delay
if (stub.delay && stub.delay > 0) {
await new Promise((resolve) => setTimeout(resolve, stub.delay))
}
// Set headers
if (stub.headers) {
Object.entries(stub.headers).forEach(([key, value]) => {
res.setHeader(key, value)
})
}
res.status(stub.statusCode).json(stub.body)
})
const PORT = process.env.MOCK_PORT ?? '3001'
app.listen(PORT, () => {
console.log(`Mock server running on port ${PORT}`)
})
export { app }
7.2 File-Based Response Routing
// Manage responses via directory structure
// responses/
// GET/
// api/
// users/
// index.json -> GET /api/users
// 1.json -> GET /api/users/1
// POST/
// api/
// users/
// index.json -> POST /api/users (response template)
app.use((req: Request, res: Response) => {
const responsePath = path.join(__dirname, 'responses', req.method, req.path, 'index.json')
if (fs.existsSync(responsePath)) {
const responseData = JSON.parse(fs.readFileSync(responsePath, 'utf-8'))
res.json(responseData)
} else {
res.status(404).json({ error: 'No response file found' })
}
})
7.3 Middleware for Delay and Error Injection
// chaos-middleware.ts - middleware for chaos engineering
interface ChaosConfig {
errorRate: number // 0.0 to 1.0
minDelay: number // ms
maxDelay: number // ms
}
let chaosConfig: ChaosConfig = {
errorRate: 0,
minDelay: 0,
maxDelay: 0,
}
export const chaosMiddleware = async (req: Request, res: Response, next: NextFunction) => {
// Response delay
const delay = chaosConfig.minDelay + Math.random() * (chaosConfig.maxDelay - chaosConfig.minDelay)
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
// Random error injection
if (Math.random() < chaosConfig.errorRate) {
return res.status(500).json({
error: 'Chaos monkey strikes!',
timestamp: new Date().toISOString(),
})
}
next()
}
// Config update API
app.post('/__admin/chaos', (req: Request, res: Response) => {
chaosConfig = { ...chaosConfig, ...req.body }
res.json({ message: 'Chaos config updated', config: chaosConfig })
})
8. Docker Compose Environment for Mock Servers
8.1 docker-compose.yml
version: '3.8'
services:
# json-server mock
json-server:
image: clue/json-server
volumes:
- ./mock-data/db.json:/data/db.json
- ./mock-data/routes.json:/data/routes.json
ports:
- '3001:80'
command: --watch db.json --routes routes.json
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost/users']
interval: 10s
timeout: 5s
retries: 3
# WireMock server
wiremock:
image: wiremock/wiremock:latest
ports:
- '8080:8080'
volumes:
- ./wiremock/mappings:/home/wiremock/mappings
- ./wiremock/__files:/home/wiremock/__files
command:
- '--verbose'
- '--global-response-templating'
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/__admin/health']
interval: 10s
timeout: 5s
retries: 3
# Application (uses mock servers as dependencies)
app:
build: .
ports:
- '8000:8000'
environment:
- USER_SERVICE_URL=http://json-server:80
- PAYMENT_SERVICE_URL=http://wiremock:8080
depends_on:
json-server:
condition: service_healthy
wiremock:
condition: service_healthy
# E2E test runner
e2e-tests:
build:
context: .
dockerfile: Dockerfile.test
environment:
- APP_URL=http://app:8000
depends_on:
- app
command: npm run test:e2e
profiles:
- test
8.2 Test Execution Script
#!/bin/bash
# run-tests.sh
echo "Starting mock server environment..."
docker-compose up -d json-server wiremock
echo "Waiting for health checks..."
docker-compose exec json-server wget -qO- http://localhost/users
docker-compose exec wiremock wget -qO- http://localhost:8080/__admin/health
echo "Starting application..."
docker-compose up -d app
echo "Running E2E tests..."
docker-compose run --rm e2e-tests
echo "Cleaning up..."
docker-compose down
8.3 Mock Data Initialization Script
// scripts/setup-mock-data.ts
const WIREMOCK_URL = 'http://localhost:8080/__admin/mappings'
const stubs = [
{
request: {
method: 'POST',
url: '/api/payment/charge',
},
response: {
status: 200,
headers: { 'Content-Type': 'application/json' },
jsonBody: {
paymentId: 'TEST-PAYMENT-001',
status: 'SUCCESS',
amount: 100,
},
},
},
{
request: {
method: 'POST',
url: '/api/payment/charge',
headers: {
'X-Simulate-Failure': { equalTo: 'true' },
},
},
response: {
status: 402,
headers: { 'Content-Type': 'application/json' },
jsonBody: {
error: 'PAYMENT_DECLINED',
message: 'Card was declined',
},
},
},
]
async function setupMockData() {
for (const stub of stubs) {
const response = await fetch(WIREMOCK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(stub),
})
console.log(`Stub registered: ${response.status}`)
}
}
setupMockData().catch(console.error)
9. Quiz
Quiz 1: What is the key difference between MSW and other mocking approaches like axios-mock-adapter?
Answer: MSW uses a Service Worker to intercept requests at the network level, so it works regardless of which HTTP library the application uses. You can also reuse the same handlers in both the browser and Node.js.
Explanation: Libraries like axios-mock-adapter mock the HTTP client itself, so changing the HTTP client requires rewriting your mock code. MSW intercepts at the actual network layer, meaning it works with fetch, axios, or any other HTTP client transparently. This also makes your mocks more realistic because the full HTTP stack is exercised.
Quiz 2: What is the main limitation of json-server, and how can you overcome it?
Answer: The main limitation of json-server is that it is difficult to implement complex business logic beyond simple CRUD operations.
Explanation: To overcome this, you can use json-server as a library and attach custom routes (server.post('/auth/login', ...)), or write custom middleware to handle more complex logic. For significantly complex scenarios, switching to WireMock or a custom-built mock server is a better approach.
Quiz 3: In what situations is WireMock's Scenarios feature most useful?
Answer: Scenarios are most useful for testing workflows where different responses should be returned based on current state — for example, an order status flow (payment pending → paid → shipping → delivered) or retry logic (first request fails, second succeeds).
Explanation: A standard stub always returns the same response. With Scenarios, each incoming request transitions the state machine to the next state and returns a response appropriate for that state. This is essential for testing non-idempotent APIs and stateful workflows.
Quiz 4: What is the primary advantage of using Prism?
Answer: Prism's main advantage is that it automatically generates a mock server from an OpenAPI spec with no additional mocking code. In Validation mode, it also verifies that real API requests and responses conform to the spec.
Explanation: Prism is particularly valuable in an API-First Design workflow. Once the API spec is defined, the frontend can immediately start development using Prism, and once the backend is complete, Validation mode can automatically verify spec compliance. This eliminates spec drift between documentation and implementation.
Quiz 5: Why should mock servers be combined with Contract Testing?
Answer: Mock servers can drift out of sync with real APIs. Combining with Contract Testing automatically verifies that the mock server matches the real API, preventing bugs caused by mismatches between the mock and the real service.
Explanation: If you develop against a mock server but the real API changes, tests pass in development but fail in production. Tools like Pact let the consumer (frontend) define its expectations as a contract, and the provider (backend) automatically verifies it satisfies those expectations. This creates a safety net that catches integration bugs before deployment.