Skip to content
Published on

[DevOps] StubとMockingサーバー完全ガイド:概念から実践まで

Authors

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. ツール比較表

機能WireMockMockServerjson-serverPrismmitmproxy
言語JavaJavaNode.jsNode.jsPython
設定方式JSONファイル/APIAPIJSONファイルOpenAPIスペックPythonスクリプト
Request Matching非常に強力非常に強力基本スペックベーススクリプトベース
Response TemplatingHandlebarsVelocity/Mustache限定的スペックベース自由
Record/PlaybackOOXXO
Stateful BehaviorO(シナリオ)OO(CRUD)XO(スクリプト)
Docker対応OOOOO
Fault InjectionOOXXO
学習コスト中程度中程度非常に低い低い中程度
適した用途汎用統合テスト動的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サーバーを自動管理する