- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. Test Doubleとは
- 2. Mockingサーバーが必要な理由
- 3. 主要なMockingサーバーツール
- 4. ツール比較表
- 5. WireMock実践:Docker Compose環境
- 6. API契約テスト(Contract Testing)
- 7. CI/CD統合
- 8. ベストプラクティス
- 9. 高度なパターン:Service Virtualization
- 10. まとめ
1. Test Doubleとは
ソフトウェアテストにおけるTest Doubleは、実際の依存コンポーネントを置き換えるオブジェクトの総称です。Martin Fowlerの分類によると、Test Doubleは大きく5つに分かれます。
1.1 Dummy
テストでパラメータを埋めるために渡すが、実際には使用されないオブジェクトです。メソッドシグネチャを満たすためだけに存在します。
// Dummy例:実際に呼び出されないlogger
public class DummyLogger implements Logger {
@Override
public void log(String message) {
// 何もしない
}
}
1.2 Stub
Stubは、あらかじめ決められた応答を返すTest Doubleです。呼び出しに対して固定データを返し、状態検証(State Verification) に使用されます。
// Stub例:常に固定ユーザーを返す
public class StubUserRepository implements UserRepository {
@Override
public User findById(Long id) {
return new User(1L, "testuser", "test@example.com");
}
}
主要な特性:
- あらかじめプログラムされた応答のみ返す
- 呼び出し回数や順序を検証しない
- テスト後に結果の状態を確認して検証
1.3 Mock
Mockは、呼び出し自体を検証するTest Doubleです。特定のメソッドが特定の引数で呼び出されたか、何回呼び出されたかなど振る舞い検証(Behavior Verification) に焦点を当てます。
// Mockitoを使用したMock例
@Test
void shouldSendWelcomeEmail() {
EmailService mockEmailService = mock(EmailService.class);
UserService userService = new UserService(mockEmailService);
userService.registerUser("test@example.com");
// 振る舞い検証:sendWelcomeEmailが正確に1回呼び出されたか確認
verify(mockEmailService, times(1))
.sendWelcomeEmail("test@example.com");
}
主要な特性:
- 期待する呼び出し(expectation)を事前に設定
- 呼び出し回数、順序、引数を検証
- 期待と異なる呼び出しの場合テスト失敗
1.4 Fake
Fakeは、実際に動作する軽量な実装体です。本番環境では使用しませんが、テスト目的には十分に機能します。
// Fake例:In-Memoryデータベース
public class FakeUserRepository implements UserRepository {
private final Map<Long, User> store = new HashMap<>();
private long sequence = 1L;
@Override
public User save(User user) {
user.setId(sequence++);
store.put(user.getId(), user);
return user;
}
@Override
public User findById(Long id) {
return store.get(id);
}
@Override
public List<User> findAll() {
return new ArrayList<>(store.values());
}
}
代表的なFake例:
- H2のようなインメモリデータベース
- ローカルファイルシステムベースのストレージ
- インメモリメッセージキュー
1.5 Spy
Spyは、実際のオブジェクトをラップして呼び出し記録を残すTest Doubleです。実際の動作を実行しながら同時に呼び出し情報を記録します。
// Mockito Spy例
@Test
void spyExample() {
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
spyList.add("hello"); // 実際に要素が追加される
verify(spyList).add("hello"); // 呼び出し記録確認
assertEquals(1, spyList.size()); // 実際の動作確認
}
1.6 比較まとめ表
| 区分 | 動作 | 検証方式 | 代表例 |
|---|---|---|---|
| Dummy | なし(パラメータ埋め用) | なし | 空の実装体 |
| Stub | 固定応答を返す | 状態検証 | ハードコードされた応答 |
| Mock | 呼び出し期待を設定 | 振る舞い検証 | Mockito mock |
| Fake | 実際の軽量実装 | 状態検証 | In-memory DB |
| Spy | 実際の動作 + 記録 | 振る舞い + 状態 | Mockito spy |
2. Mockingサーバーが必要な理由
マイクロサービスアーキテクチャで外部APIに依存するサービスをテストする際、以下の問題が発生します。
- 外部サービスの不安定性:外部APIがダウンすると自分のテストも失敗
- レートリミット:外部APIの呼び出し制限でテストの繰り返しが不可能
- コスト:有料APIをテストのたびに呼び出すとコストが発生
- 速度:ネットワーク遅延でテスト時間が増加
- エッジケースの再現:特定のエラー応答、タイムアウトなどの再現が困難
- 開発環境の独立性:外部サービスなしでも開発可能であるべき
Mockingサーバーはこれらすべての問題を解決します。実際の外部サービスと同じインターフェースを提供しながら予測可能な応答を返します。
┌──────────────┐ ┌──────────────┐ ┌────────────────┐
│ テストコード │─────>│ 自サービス │─────>│ Mockingサーバー │
│ │ │ │ │ (WireMock等) │
└──────────────┘ └──────────────┘ └────────────────┘
│
事前定義された応答を返す
3. 主要なMockingサーバーツール
3.1 WireMock
WireMockは最も強力で機能豊富なオープンソースMockサーバーです。Javaベースですがスタンドアロンでも実行できます。
主要機能:
- JSONベースのStubマッピングファイル
- 強力なRequest Matching(URL、ヘッダー、ボディ、クエリパラメータ)
- Response Templating(Handlebarsベース)
- Record/Playbackモード(実際のAPIトラフィック録画)
- Stateful Behavior(シナリオベースの状態遷移)
- Fault Injection(遅延、接続切断シミュレーション)
- Dockerイメージ提供
DockerでWireMock実行:
docker run -d --name wiremock \
-p 8080:8080 \
-v $(pwd)/stubs:/home/wiremock \
wiremock/wiremock:latest \
--global-response-templating \
--verbose
基本的なStubマッピングファイル構造:
stubs/mappings/get-user.json:
{
"request": {
"method": "GET",
"urlPathPattern": "/api/users/[0-9]+",
"headers": {
"Accept": {
"contains": "application/json"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
}
}
Request Matchingパターン:
{
"request": {
"method": "POST",
"urlPath": "/api/orders",
"bodyPatterns": [
{
"matchesJsonPath": "$.items[?(@.quantity > 0)]"
},
{
"matchesJsonPath": {
"expression": "$.customerId",
"regex": "^[A-Z]{2}[0-9]{6}$"
}
}
],
"queryParameters": {
"status": {
"equalTo": "active"
}
}
},
"response": {
"status": 201,
"jsonBody": {
"orderId": "ORD-001",
"status": "created"
}
}
}
Response Templating例:
{
"request": {
"method": "GET",
"urlPathTemplate": "/api/users/{userId}"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"id": "{{request.pathSegments.[2]}}",
"requestedAt": "{{now}}",
"userAgent": "{{request.headers.User-Agent}}"
},
"transformers": ["response-template"]
}
}
Stateful Behavior(シナリオ):
{
"mappings": [
{
"scenarioName": "OrderFlow",
"requiredScenarioState": "Started",
"newScenarioState": "OrderCreated",
"request": {
"method": "POST",
"urlPath": "/api/orders"
},
"response": {
"status": 201,
"jsonBody": { "status": "created" }
}
},
{
"scenarioName": "OrderFlow",
"requiredScenarioState": "OrderCreated",
"request": {
"method": "GET",
"urlPath": "/api/orders/1"
},
"response": {
"status": 200,
"jsonBody": { "status": "processing" }
}
}
]
}
3.2 MockServer
MockServerはJavaベースのMockサーバーで、WireMockとは差別化された機能を提供します。
Docker実行:
docker run -d --name mockserver \
-p 1080:1080 \
mockserver/mockserver:latest
Expectation設定(REST API):
curl -X PUT "http://localhost:1080/mockserver/expectation" \
-H "Content-Type: application/json" \
-d '{
"httpRequest": {
"method": "GET",
"path": "/api/products",
"queryStringParameters": {
"category": ["electronics"]
}
},
"httpResponse": {
"statusCode": 200,
"headers": {
"Content-Type": ["application/json"]
},
"body": {
"type": "JSON",
"json": "[{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}]"
}
},
"times": {
"unlimited": true
}
}'
3.3 json-server
json-serverはNode.jsベースの軽量REST APIサーバーです。JSONファイル一つで即座にREST APIを提供します。
npm install -g json-server
cat > db.json << 'JSONEOF'
{
"users": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
],
"posts": [
{ "id": 1, "title": "Hello World", "userId": 1 },
{ "id": 2, "title": "Testing Guide", "userId": 2 }
]
}
JSONEOF
json-server --watch db.json --port 3001
自動生成されるエンドポイント:
GET /users # 一覧取得
GET /users/1 # 個別取得
POST /users # 作成
PUT /users/1 # 全体更新
PATCH /users/1 # 部分更新
DELETE /users/1 # 削除
GET /users?name=Alice # フィルタリング
GET /users?_sort=name&_order=asc # ソート
GET /users?_page=1&_limit=10 # ページネーション
3.4 Prism(Stoplight)
PrismはOpenAPI(Swagger)スペックに基づいて自動的にMockサーバーを生成するツールです。
npm install -g @stoplight/prism-cli
# OpenAPIスペックファイルでMockサーバー実行
prism mock openapi.yaml --port 4010
# 動的応答生成モード
prism mock openapi.yaml --dynamic --port 4010
3.5 mitmproxy
mitmproxyはHTTP/HTTPSプロキシで、実際のトラフィックをインターセプトして修正できます。
# modify_response.py
import json
from mitmproxy import http
def response(flow: http.HTTPFlow) -> None:
if "/api/users" in flow.request.pretty_url:
flow.response.status_code = 200
flow.response.set_text(json.dumps([
{"id": 1, "name": "Mocked User"}
]))
flow.response.headers["Content-Type"] = "application/json"
4. ツール比較表
| 機能 | WireMock | MockServer | json-server | Prism | mitmproxy |
|---|---|---|---|---|---|
| 言語 | Java | Java | Node.js | Node.js | Python |
| 設定方式 | JSONファイル/API | API | JSONファイル | OpenAPIスペック | Pythonスクリプト |
| Request Matching | 非常に強力 | 非常に強力 | 基本 | スペックベース | スクリプトベース |
| Response Templating | Handlebars | Velocity/Mustache | 限定的 | スペックベース | 自由 |
| Record/Playback | O | O | X | X | O |
| Stateful Behavior | O(シナリオ) | O | O(CRUD) | X | O(スクリプト) |
| Docker対応 | O | O | O | O | O |
| Fault Injection | O | O | X | X | O |
| 学習コスト | 中程度 | 中程度 | 非常に低い | 低い | 中程度 |
| 適した用途 | 汎用統合テスト | 動的APIテスト | フロントエンド開発 | API-First開発 | デバッグ/分析 |
5. WireMock実践:Docker Compose環境
5.1 プロジェクト構造
project/
docker-compose.yaml
wiremock/
mappings/
get-users.json
post-order.json
__files/
user-list.json
5.2 Docker Compose設定
version: '3.8'
services:
wiremock:
image: wiremock/wiremock:latest
ports:
- '8080:8080'
volumes:
- ./wiremock/mappings:/home/wiremock/mappings
- ./wiremock/__files:/home/wiremock/__files
command:
- '--global-response-templating'
- '--verbose'
- '--disable-gzip'
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8080/__admin/health']
interval: 10s
timeout: 5s
retries: 3
my-service:
build: .
environment:
- EXTERNAL_API_URL=http://wiremock:8080
depends_on:
wiremock:
condition: service_healthy
5.3 各種マッピング例
遅延応答シミュレーション:
{
"request": {
"method": "GET",
"urlPath": "/api/slow-endpoint"
},
"response": {
"status": 200,
"fixedDelayMilliseconds": 3000,
"jsonBody": { "message": "delayed response" }
}
}
Fault Injection(接続切断):
{
"request": {
"method": "GET",
"urlPath": "/api/fault"
},
"response": {
"fault": "CONNECTION_RESET_BY_PEER"
}
}
6. API契約テスト(Contract Testing)
6.1 Pact
Pactは消費者駆動契約テスト(Consumer-Driven Contract Testing)をサポートするフレームワークです。
┌──────────────┐ Pactファイル生成 ┌──────────────┐
│ Consumer │ ─────────────────── > │ Pact Broker │
│ (フロントエンド) │ │ │
└──────────────┘ └──────┬───────┘
│
Pactファイル検証
│
┌──────▼───────┐
│ Provider │
│ (バックエンド) │
└──────────────┘
Consumerテスト(JavaScript):
const { PactV3 } = require('@pact-foundation/pact')
const provider = new PactV3({
consumer: 'FrontendApp',
provider: 'UserService',
})
describe('User API', () => {
it('should return user by ID', async () => {
await provider
.given('a user with ID 1 exists')
.uponReceiving('a request for user 1')
.withRequest({
method: 'GET',
path: '/api/users/1',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: 1,
name: 'Alice',
email: 'alice@example.com',
},
})
await provider.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/api/users/1`, {
headers: { Accept: 'application/json' },
})
const user = await response.json()
expect(user.name).toBe('Alice')
})
})
})
6.2 Spring Cloud Contract
Contract.make {
description "should return user by ID"
request {
method GET()
url "/api/users/1"
headers {
accept(applicationJson())
}
}
response {
status OK()
headers {
contentType(applicationJson())
}
body([
id: 1,
name: "Alice",
email: "alice@example.com"
])
}
}
7. CI/CD統合
7.1 Testcontainers + WireMock
import org.wiremock.integrations.testcontainers.WireMockContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@SpringBootTest
class ExternalApiIntegrationTest {
@Container
static WireMockContainer wiremock = new WireMockContainer(
"wiremock/wiremock:latest"
)
.withMappingFromResource("mappings/get-user.json");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("external.api.url", wiremock::getBaseUrl);
}
@Autowired
private UserClient userClient;
@Test
void shouldFetchUserFromExternalApi() {
User user = userClient.getUser(1L);
assertThat(user.getName()).isEqualTo("John Doe");
}
}
7.2 GitHub ActionsでのWireMock使用
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
wiremock:
image: wiremock/wiremock:latest
ports:
- 8080:8080
options: >-
--health-cmd "curl -f http://localhost:8080/__admin/health"
--health-interval 10s
--health-timeout 5s
--health-retries 3
steps:
- uses: actions/checkout@v4
- name: Run Integration Tests
run: |
./gradlew integrationTest
env:
EXTERNAL_API_URL: http://localhost:8080
8. ベストプラクティス
8.1 Stubファイル管理
- StubマッピングファイルをGitでバージョン管理する
- 環境ごとに異なるマッピングセットを準備する
- マッピングファイル名に意味のある名前を使用する
8.2 リアルなMockデータの作成
const { faker } = require('@faker-js/faker')
function generateMockUsers(count) {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: faker.person.fullName(),
email: faker.internet.email(),
phone: faker.phone.number(),
address: faker.location.streetAddress(),
createdAt: faker.date.past().toISOString(),
}))
}
8.3 エラーシナリオテスト
常に以下のエラー状況に対するStubを準備すべきです:
- 4xxエラー:400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、429 Too Many Requests
- 5xxエラー:500 Internal Server Error、502 Bad Gateway、503 Service Unavailable
- ネットワークエラー:Connection timeout、Connection reset、空の応答
- 遅い応答:タイムアウト閾値を超える遅延
8.4 Mockサーバー選択ガイドライン
- フロントエンド開発:json-server(素早いスタート、自動CRUD対応)
- API-First開発:Prism(OpenAPIスペックベースの自動Mock)
- Java/Spring統合テスト:WireMock(Testcontainers統合、強力なマッチング)
- 動的シナリオテスト:MockServer(ランタイムExpectation変更)
- トラフィック分析/デバッグ:mitmproxy(実際のトラフィックインターセプト)
- 契約テスト:Pact(Consumer-Driven)またはSpring Cloud Contract(Provider-Driven)
9. 高度なパターン:Service Virtualization
MockingサーバーをさらにService Virtualizationに拡張すると、実際のサービスの複雑な動作を完全にシミュレーションできます。
9.1 WireMock + Stateful Scenarioでワークフローシミュレーション
{
"mappings": [
{
"scenarioName": "PaymentFlow",
"requiredScenarioState": "Started",
"newScenarioState": "PaymentPending",
"request": { "method": "POST", "urlPath": "/api/payments" },
"response": {
"status": 201,
"jsonBody": { "paymentId": "PAY-001", "status": "pending" }
}
},
{
"scenarioName": "PaymentFlow",
"requiredScenarioState": "PaymentPending",
"newScenarioState": "PaymentCompleted",
"request": { "method": "POST", "urlPath": "/api/payments/PAY-001/confirm" },
"response": {
"status": 200,
"jsonBody": { "paymentId": "PAY-001", "status": "completed" }
}
},
{
"scenarioName": "PaymentFlow",
"requiredScenarioState": "PaymentCompleted",
"request": { "method": "GET", "urlPath": "/api/payments/PAY-001" },
"response": {
"status": 200,
"jsonBody": { "paymentId": "PAY-001", "status": "completed", "amount": 99.99 }
}
}
]
}
9.2 WireMock Admin APIの活用
# 全マッピングの照会
curl http://localhost:8080/__admin/mappings
# リクエストログの照会
curl http://localhost:8080/__admin/requests
# シナリオ状態のリセット
curl -X POST http://localhost:8080/__admin/scenarios/reset
# 全マッピングのリセット
curl -X POST http://localhost:8080/__admin/mappings/reset
10. まとめ
Mockingサーバーは現代のソフトウェア開発において必須のツールです。適切なツールを選択し正しく活用すれば、開発速度を向上させ、テストの信頼性を確保し、CI/CDパイプラインを安定化できます。
要点整理:
- Stub vs Mockの違いを理解し状況に合わせて使用する
- WireMockは最も汎用的で強力な選択肢
- json-serverはフロントエンド開発に素早く適用できる
- PrismはAPI-Firstワークフローに最適
- 契約テストとMockサーバーを組み合わせるとマイクロサービステスト戦略が完成する
- CI/CDではTestcontainersを活用してMockサーバーを自動管理する