LESSON 30分

ストーリー

あなた
各サービスの単体テストは全部パスしてるのに、統合したら動きません…
高橋アーキテクト
モノリスのテストピラミッドでは不十分だ。マイクロサービスにはContract TestingとChaos Engineeringという新しいテスト戦略が必要だよ

マイクロサービスのテストピラミッド

        /\
       /  \     E2E Tests(少数)
      /    \    → 全体フロー確認
     /──────\
    /        \  Integration Tests
   /          \ → サービス+DB, サービス+MQ
  /────────────\
 /              \  Contract Tests(重要!)
/                \ → サービス間の契約検証
/──────────────────\
                    Unit Tests(多数)
                    → ビジネスロジック

Contract Testing(契約テスト)

サービス間のAPI契約が正しく守られていることを検証するテストです。

Consumer-Driven Contract Testing

// Consumer(利用側)が期待する契約を定義
// → Provider(提供側)がその契約を満たすことを検証

// 1. Consumer側: 期待する契約(Pact)を定義
// order-service が user-service に期待すること
const pact = new Pact({
  consumer: "order-service",
  provider: "user-service",
});

describe("User Service Contract", () => {
  it("should return user by ID", async () => {
    // 期待するリクエストとレスポンスを定義
    await pact.addInteraction({
      state: "user usr-123 exists",
      uponReceiving: "a request for user usr-123",
      withRequest: {
        method: "GET",
        path: "/users/usr-123",
        headers: { Accept: "application/json" },
      },
      willRespondWith: {
        status: 200,
        headers: { "Content-Type": "application/json" },
        body: {
          id: like("usr-123"),
          name: like("田中太郎"),
          email: like("tanaka@example.com"),
          // like() = 型と構造のみ検証、値は問わない
        },
      },
    });

    // Consumerのコードをモックサーバーに対して実行
    const user = await userClient.getUser("usr-123");
    expect(user.id).toBe("usr-123");
    expect(user.name).toBeDefined();
  });
});
// → Pactファイル(契約)が生成される

// 2. Provider側: 契約を検証
describe("User Service Provider Verification", () => {
  it("should fulfill the contract with order-service", async () => {
    const verifier = new Verifier({
      provider: "user-service",
      providerBaseUrl: "http://localhost:3000",
      pactUrls: ["./pacts/order-service-user-service.json"],
      stateHandlers: {
        "user usr-123 exists": async () => {
          // テスト用のデータをセットアップ
          await seedUser({ id: "usr-123", name: "田中太郎" });
        },
      },
    });

    await verifier.verifyProvider();
    // → すべての契約が満たされていることを確認
  });
});

イベント契約テスト

// イベントの契約もテスト可能
describe("Order Created Event Contract", () => {
  it("should produce valid order.created event", async () => {
    // Producer側: イベントのスキーマを検証
    const event = await orderService.createOrder(testData);

    // CloudEvents仕様に準拠しているか
    expect(event.specversion).toBe("1.0");
    expect(event.type).toBe("com.example.order.created");
    expect(event.data.orderId).toBeDefined();
    expect(event.data.userId).toBeDefined();
    expect(event.data.items).toBeInstanceOf(Array);
    expect(event.data.totalAmount).toBeGreaterThan(0);
  });

  it("consumer should handle order.created event", async () => {
    // Consumer側: イベントを正しく処理できるか
    const event: OrderCreatedEvent = {
      specversion: "1.0",
      id: "evt-001",
      source: "/order-service",
      type: "com.example.order.created",
      time: new Date().toISOString(),
      data: {
        orderId: "ord-001",
        userId: "usr-001",
        items: [{ productId: "prod-001", quantity: 2, unitPrice: 500 }],
        totalAmount: 1000,
      },
    };

    await paymentHandler.handleOrderCreated(event);
    // → 決済処理が正しく開始されたことを検証
  });
});

Chaos Engineering(カオスエンジニアリング)

意図的に障害を注入し、システムの耐障害性を検証するアプローチです。

基本的な原則

1. 定常状態を定義する(正常時のメトリクス)
2. 仮説を立てる(「サービスAが落ちても注文は処理される」)
3. 障害を注入する(サービスAを停止)
4. 結果を観察する(仮説と比較)
5. 改善する(発見した脆弱性を修正)

障害注入のパターン

// 障害注入の種類
const chaosExperiments = {
  // 1. サービスの停止
  serviceKill: {
    action: "Payment Serviceの全インスタンスを停止",
    hypothesis: "注文サービスがサーキットブレーカーで適切にフォールバック",
    verify: "注文が『決済保留』状態で保存され、後で再処理される",
  },

  // 2. ネットワーク遅延
  latencyInjection: {
    action: "Inventory Serviceへの通信に3秒の遅延を追加",
    hypothesis: "タイムアウト設定が適切に動作する",
    verify: "3秒以内にタイムアウトし、リトライまたはフォールバック",
  },

  // 3. ネットワーク分断
  networkPartition: {
    action: "Order ServiceとPayment Service間の通信を遮断",
    hypothesis: "イベントがメッセージキューにバッファリングされる",
    verify: "通信復旧後にイベントが処理される",
  },

  // 4. リソース枯渇
  resourceExhaustion: {
    action: "サービスのCPU使用率を90%に上げる",
    hypothesis: "オートスケーリングが発動する",
    verify: "2分以内に新しいインスタンスが追加される",
  },

  // 5. データ破損
  dataCorruption: {
    action: "Read Modelのデータを一部削除",
    hypothesis: "リビルドプロセスで復旧できる",
    verify: "イベントストアからRead Modelが再構築される",
  },
};

主要なツール

ツールプラットフォーム特徴
Chaos MonkeyNetflix OSSランダムにインスタンスを停止
LitmusKubernetesK8s向けカオス実験
AWS Fault Injection SimulatorAWSマネージドカオス
Gremlinマルチプラットフォーム商用、高機能
Toxiproxy汎用ネットワーク障害シミュレーション

オブザーバビリティ

テストの結果を観察するための3本柱です。

// 分散トレーシング: リクエストの流れを追跡
const tracing = {
  tool: "OpenTelemetry + Jaeger",
  purpose: "1つのリクエストが複数サービスを通過する流れを可視化",
  example: "注文API → 決済サービス → 在庫サービス → 通知サービス",
};

// メトリクス: 数値指標の収集
const metrics = {
  tool: "Prometheus + Grafana",
  purpose: "レイテンシ、エラー率、スループットを監視",
  goldSignals: ["Latency", "Traffic", "Errors", "Saturation"],
};

// ログ: 構造化ログの集約
const logging = {
  tool: "ELK Stack (Elasticsearch + Logstash + Kibana)",
  purpose: "全サービスのログを集約して検索",
  key: "correlationIdで関連ログを紐づけ",
};

まとめ

ポイント内容
Contract Testingサービス間のAPI/イベント契約を検証
Chaos Engineering意図的に障害を注入して耐障害性を検証
オブザーバビリティトレーシング、メトリクス、ログの3本柱
テストの階層Unit → Contract → Integration → E2E

チェックリスト

  • Consumer-Driven Contract Testingの仕組みを説明できる
  • Chaos Engineeringの5つのステップを理解した
  • 障害注入のパターンを3つ以上挙げられる
  • オブザーバビリティの3本柱を説明できる

次のステップへ

次は演習で、CQRSシステムの設計と結果整合性の戦略を実践してみましょう。


推定読了時間: 30分