- Authors

- Name
- Youngju Kim
- @fjvbn20031
はじめに
モックサーバーは、バックエンドの準備が整っていない段階でもフロントエンド開発を進めたり、外部 API への依存を排除してテストを実行したりするために欠かせないツールです。本ガイドでは WireMock・MSW・json-server・Prism の4つの主要ツールを実践的に解説します。
1. モックサーバーとは:スタブ・フェイク・モックの違い
テストダブルにはいくつかの種類があります。
| 種類 | 説明 | 例 |
|---|---|---|
| スタブ (Stub) | 決まったレスポンスを返すだけ | GET /users/1 → 固定 JSON |
| フェイク (Fake) | 動作する簡易実装 | インメモリ DB |
| モック (Mock) | 呼び出しを検証できるスタブ | WireMock の verify |
| スパイ (Spy) | 実装を持ちつつ呼び出しを記録 | Mockito の @Spy |
モックサーバーは主に「スタブ」と「モック」の機能を提供し、HTTP レイヤーで外部 API をエミュレートします。
モックサーバーを使う理由
- バックエンドが未完成でもフロントエンド開発を並行できる
- 外部 API の障害・レート制限・コストを回避できる
- エラーケース・遅延などの再現が困難なシナリオをテストできる
- CI/CD 環境で外部依存なしにテストを実行できる
2. WireMock スタンドアロン
JAR を使った起動
WireMock はスタンドアロン JAR として起動できます。
# JAR のダウンロード
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
# 起動 (デフォルトポート 8080)
java -jar wiremock.jar
# カスタムポートで起動
java -jar wiremock.jar --port 9000 --https-port 9443
# レコーディングモード(実際の API をプロキシしてレスポンスを記録)
java -jar wiremock.jar --proxy-all "https://api.example.com" --record-mappings
mappings フォルダによる JSON 設定
WireMock は mappings/ フォルダに JSON ファイルを置くだけでスタブを定義できます。
wiremock/
mappings/
get-users.json
post-order.json
get-product-by-id.json
__files/
users-response.json
order-response.json
// mappings/get-users.json
{
"request": {
"method": "GET",
"url": "/api/users"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "users-response.json"
}
}
// mappings/get-product-by-id.json(URL パターンマッチング)
{
"request": {
"method": "GET",
"urlPathPattern": "/api/products/[0-9]+"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"body": "{\"id\": 1, \"name\": \"サンプル商品\", \"price\": 1500}"
}
}
動的レスポンス(テンプレート)
// mappings/dynamic-response.json
{
"request": {
"method": "POST",
"url": "/api/echo"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"body": "{ \"received\": \"{{jsonPath request.body '$.message'}}\" }",
"transformers": ["response-template"]
}
}
クイズ1: WireMock のスタンドアロンモードとライブラリモードの違いは?
答え: スタンドアロンモードは独立した JAR プロセスとして起動し、どの言語からでも利用できます。ライブラリモードは Java プロジェクトに依存として追加し、テストコード内でプログラム的に制御します。
解説: スタンドアロンモードは JSON ファイルで設定を管理するため、フロントエンド開発者が Java を知らなくても使えるメリットがあります。ライブラリモードはテストコードと設定を同じ言語で管理でき、動的なスタブ生成が容易です。チームの構成や用途によって使い分けます。
3. WireMock Java: JUnit5 との統合
@WireMockTest アノテーション
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
@WireMockTest(httpPort = 8089)
class PaymentApiClientTest {
private PaymentApiClient client;
@BeforeEach
void setUp() {
client = new PaymentApiClient("http://localhost:8089");
}
@Test
void chargeCard_success_returnsTransactionId() {
stubFor(post(urlEqualTo("/payments/charge"))
.withRequestBody(matchingJsonPath("$.amount", equalTo("5000")))
.withRequestBody(matchingJsonPath("$.currency", equalTo("JPY")))
.withHeader("Authorization", matching("Bearer .+"))
.willReturn(aResponse()
.withStatus(201)
.withHeader("Content-Type", "application/json")
.withBody("{\"transactionId\": \"txn_abc123\", \"status\": \"SUCCESS\"}")));
String txnId = client.charge(5000, "JPY", "Bearer token123");
assertThat(txnId).isEqualTo("txn_abc123");
verify(postRequestedFor(urlEqualTo("/payments/charge")));
}
@Test
void chargeCard_networkError_retriesThreeTimes() {
stubFor(post(urlEqualTo("/payments/charge"))
.willReturn(aResponse()
.withFault(Fault.CONNECTION_RESET_BY_PEER)));
assertThatThrownBy(() -> client.charge(1000, "JPY", "Bearer token"))
.isInstanceOf(PaymentNetworkException.class);
verify(3, postRequestedFor(urlEqualTo("/payments/charge")));
}
}
WireMockExtension を使った細かい制御
@ExtendWith(WireMockExtension.class)
class AdvancedWireMockTest {
@RegisterExtension
static WireMockExtension wm = WireMockExtension.newInstance()
.options(wireMockConfig()
.port(8090)
.httpsPort(8091)
.withRootDirectory("src/test/resources/wiremock"))
.build();
@Test
void testWithCustomConfig() {
wm.stubFor(get("/api/data")
.willReturn(okJson("{\"key\": \"value\"}")));
// テスト実行後のリクエスト履歴を取得
List<LoggedRequest> requests = wm.findAll(getRequestedFor(anyUrl()));
assertThat(requests).isNotEmpty();
}
}
クイズ2: WireMock の Fault シミュレーションはいつ使うべきか?
答え: ネットワーク障害(接続リセット、タイムアウト、不正なレスポンス)に対するクライアントの耐性をテストしたい場合に使います。
解説: Fault.CONNECTION_RESET_BY_PEER(接続リセット)、Fault.EMPTY_RESPONSE(空レスポンス)、Fault.MALFORMED_RESPONSE_CHUNK(不正チャンク)などを使えます。これにより「本番環境で稀に起きる障害」を再現し、リトライロジックやフォールバックが正しく機能するかを検証できます。
4. MSW(Mock Service Worker)
ブラウザ環境のセットアップ
MSW はサービスワーカーを使ってブラウザ上で HTTP リクエストをインターセプトします。
npm install msw --save-dev
npx msw init public/ --save
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET リクエストのハンドラー
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
])
}),
// パスパラメータを使ったハンドラー
http.get('/api/users/:id', ({ params }) => {
const { id } = params
if (id === '99') {
return new HttpResponse(null, { status: 404 })
}
return HttpResponse.json({ id: Number(id), name: `User ${id}` })
}),
// POST リクエストのハンドラー
http.post('/api/users', async ({ request }) => {
const body = (await request.json()) as { name: string; email: string }
return HttpResponse.json({ id: Date.now(), ...body }, { status: 201 })
}),
// 認証エラーのシミュレーション
http.get('/api/secure', ({ request }) => {
const auth = request.headers.get('Authorization')
if (!auth?.startsWith('Bearer ')) {
return new HttpResponse(null, { status: 401 })
}
return HttpResponse.json({ data: 'secret' })
}),
]
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// src/main.tsx(開発環境でのみ有効化)
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(<App />)
})
Node.js 環境(テスト)でのセットアップ
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// vitest.setup.ts または jest.setup.ts
import { server } from './src/mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
import UserList from './UserList'
test('ユーザー一覧が表示される', async () => {
render(<UserList />)
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument()
expect(screen.getByText('Bob')).toBeInTheDocument()
})
})
test('APIエラー時にエラーメッセージが表示される', async () => {
// テスト固有のハンドラーでオーバーライド
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 })
})
)
render(<UserList />)
await waitFor(() => {
expect(screen.getByText('データの取得に失敗しました')).toBeInTheDocument()
})
})
クイズ3: MSW がサービスワーカーを使う利点は何か?
答え: サービスワーカーはネットワーク層でリクエストをインターセプトするため、アプリケーションコードを変更せずにモックを注入できます。テスト環境と本番環境で同じコードが動作します。
解説: 従来の fetch や axios をモックする方法はアプリケーションコードに密結合します。MSW はブラウザのサービスワーカー API を利用するため、実際の HTTP リクエストと同じ流れで処理され、より現実に近いテストが可能です。また、server.use() でテストごとにハンドラーを動的に変更でき、server.resetHandlers() でリセットできるため、テスト間の干渉を防げます。
5. json-server
基本セットアップ
npm install -g json-server
# または開発依存として
npm install --save-dev json-server
// db.json
{
"users": [
{ "id": 1, "name": "Alice", "email": "alice@example.com", "role": "admin" },
{ "id": 2, "name": "Bob", "email": "bob@example.com", "role": "user" }
],
"products": [
{ "id": 1, "name": "Java入門", "price": 3000, "categoryId": 1 },
{ "id": 2, "name": "Spring Boot実践", "price": 4500, "categoryId": 1 },
{ "id": 3, "name": "React完全ガイド", "price": 3800, "categoryId": 2 }
],
"categories": [
{ "id": 1, "name": "技術書" },
{ "id": 2, "name": "フロントエンド" }
],
"orders": []
}
# 起動(デフォルトポート 3000)
json-server --watch db.json
# カスタムポートで起動
json-server --watch db.json --port 4000
# 遅延を追加(ms)
json-server --watch db.json --delay 500
json-server が自動生成するエンドポイント:
GET /users- 全ユーザー取得GET /users/1- ID 指定取得POST /users- 新規作成PUT /users/1- 全体更新PATCH /users/1- 部分更新DELETE /users/1- 削除GET /users?role=admin- フィルタリングGET /users?_sort=name&_order=asc- ソートGET /users?_page=1&_limit=10- ページネーション
routes.json によるカスタムルーティング
// routes.json
{
"/api/*": "/$1",
"/api/v2/users/:id": "/users/:id",
"/blog/:year/:month": "/posts?year=:year&month=:month"
}
json-server --watch db.json --routes routes.json
カスタムミドルウェア
// middleware.js
module.exports = (req, res, next) => {
// 認証チェック
if (req.path.startsWith('/api/secure') && !req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' })
}
// レスポンスにメタ情報を追加
res.header('X-Powered-By', 'json-server-mock')
// POST リクエストにタイムスタンプを追加
if (req.method === 'POST') {
req.body.createdAt = new Date().toISOString()
}
next()
}
json-server --watch db.json --middlewares middleware.js
6. Prism: OpenAPI 仕様からの自動モック生成
セットアップと起動
npm install -g @stoplight/prism-cli
# openapi.yaml
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
summary: ユーザー一覧取得
responses:
'200':
description: 成功
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
example:
- id: 1
name: Alice
email: alice@example.com
/users/{id}:
get:
summary: ユーザー取得
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: Not Found
components:
schemas:
User:
type: object
required: [id, name, email]
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
# モックサーバーを起動
prism mock openapi.yaml
# バリデーション付きプロキシモード
prism proxy openapi.yaml https://api.example.com
# 動的データ生成(faker を使用)
prism mock openapi.yaml --dynamic
Prism の動作モード
- 静的モード:
exampleフィールドに定義したデータを返す - 動的モード: スキーマから自動でランダムデータを生成する
- プロキシモード: 実際の API へのリクエストをバリデーションしながら中継する
クイズ4: Prism のプロキシモードはどんな場面で役立つか?
答え: バックエンド API が開発中であっても OpenAPI 仕様が先に定義されている場合に、フロントエンドは仕様通りのレスポンスを受け取りながら開発を進められます。また、本番 API へのリクエストが仕様に準拠しているかを検証するためにも使えます。
解説: プロキシモードでは、リクエストとレスポンスの両方が OpenAPI スキーマに対して検証されます。仕様からの逸脱(未定義フィールド、型の不一致など)があれば警告やエラーが出力されます。これにより、フロントエンドとバックエンドの「契約」を継続的に確認できます。
7. フロントエンド開発でのモックサーバー活用パターン
パターン 1: 開発環境専用モック(MSW)
// vite.config.ts の環境変数で切り替え
// .env.development
VITE_USE_MOCK=true
// src/main.tsx
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'
async function bootstrap() {
if (USE_MOCK) {
const { worker } = await import('./mocks/browser')
await worker.start()
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
}
パターン 2: シナリオ切り替え
// 開発中にUIでシナリオを切り替える
const scenarios = {
normal: handlers,
loading: [
http.get('/api/users', async () => {
await delay(Infinity) // 永続的なローディング
return HttpResponse.json([])
}),
],
error: [
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 })
}),
],
empty: [
http.get('/api/users', () => {
return HttpResponse.json([])
}),
],
}
// DevTools でシナリオを切り替え
window.__MOCK_SCENARIO__ = 'error'
server.use(...scenarios[window.__MOCK_SCENARIO__])
パターン 3: Storybook との統合
// .storybook/preview.ts
import { initialize, mswLoader } from 'msw-storybook-addon'
initialize()
export const preview = {
loaders: [mswLoader],
}
// UserCard.stories.tsx
export const WithError: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users/1', () => {
return new HttpResponse(null, { status: 404 })
}),
],
},
},
}
8. CI/CD でのモックサーバー統合
GitHub Actions での WireMock
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Start WireMock
run: |
docker run -d \
--name wiremock \
-p 8080:8080 \
-v $PWD/wiremock:/home/wiremock \
wiremock/wiremock:3.3.1
- name: Wait for WireMock
run: |
until curl -sf http://localhost:8080/__admin/; do
echo "Waiting for WireMock..."
sleep 1
done
- name: Run tests
run: ./mvnw test -Dapi.base.url=http://localhost:8080
- name: Stop WireMock
if: always()
run: docker stop wiremock
Docker Compose でのモック環境
# docker-compose.test.yml
version: '3.8'
services:
app:
build: .
environment:
- PAYMENT_API_URL=http://payment-mock:8080
- EMAIL_API_URL=http://email-mock:8080
depends_on:
- payment-mock
- email-mock
payment-mock:
image: wiremock/wiremock:3.3.1
volumes:
- ./mocks/payment:/home/wiremock
ports:
- '8081:8080'
email-mock:
image: wiremock/wiremock:3.3.1
volumes:
- ./mocks/email:/home/wiremock
ports:
- '8082:8080'
test-runner:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
- app
command: npm test
json-server を CI で使う
// package.json
{
"scripts": {
"mock": "json-server --watch db.json --port 4000",
"test:e2e": "start-server-and-test mock http://localhost:4000 cypress:run"
},
"devDependencies": {
"json-server": "^1.0.0",
"start-server-and-test": "^2.0.0",
"cypress": "^13.0.0"
}
}
クイズ5: Contract Testing(コントラクトテスト)とモックサーバーの関係は?
答え: コントラクトテストは、コンシューマー(クライアント)とプロバイダー(サーバー)間の「契約(API仕様)」を自動的に検証します。モックサーバーはコンシューマー側のテストで使われ、プロバイダー側でその契約が本当に満たされているかを検証します。
解説: Pact などのコントラクトテストツールを使うと、コンシューマーがモックサーバーとのやりとりから「コントラクトファイル」を生成し、そのファイルをプロバイダーが検証します。これにより、モックと実際の API の乖離(「モックが嘘をついている」問題)を防ぎます。CI パイプラインに組み込むと、API の変更がコンシューマーに影響を与えるかを自動で検出できます。
ツール比較まとめ
| ツール | 言語/環境 | 向いている用途 | 設定方法 |
|---|---|---|---|
| WireMock | Java/JVM | Java バックエンドテスト | Java コード / JSON |
| MSW | Browser/Node.js | React/Vue テスト、Storybook | TypeScript ハンドラー |
| json-server | Node.js | プロトタイピング、フロント開発 | db.json |
| Prism | 任意 | OpenAPI 仕様駆動開発 | YAML/JSON 仕様ファイル |
選択指針
- Java Spring Boot テスト → WireMock(JUnit5 統合が充実)
- React/Vue フロントエンド開発 → MSW(ブラウザに最も近い動作)
- 素早いプロトタイプ・デモ → json-server(最短で起動可能)
- API ファースト開発 → Prism(OpenAPI 仕様から自動生成)
- 複数マイクロサービスのテスト → WireMock スタンドアロン + Docker Compose
まとめ
モックサーバーを適切に活用することで、開発速度の向上とテスト品質の改善を同時に達成できます。フロントエンドとバックエンドの並行開発、外部 API への依存排除、エッジケースの再現など、様々なシナリオでモックサーバーは威力を発揮します。各ツールの特性を理解し、プロジェクトのニーズに合った選択をしましょう。