•18 min read

PactとNode.jsによるMicroservicesのContract Testing

PactとNode.jsによるMicroservicesのContract Testing

適切なAPIテスト戦略を選択することは、マイクロサービスリリースがスムーズに実行されるか、本番デプロイ移行中に失敗するかを決定します。コンシューマー主導の契約テスト(Consumer-driven contract testing)は、重いエンドツーエンド環境クラスターを立ち上げるオーバーヘッドなしに、マイクロサービス間の壊れたAPI統合を排除します。Node.jsサービスでPactを使用すると、コンシューマーアプリケーションはHTTPインタラクション契約を定義でき、プロバイダーは継続的インテグレーションパイプラインで独立して検証し、安全な独立デプロイを可能にします。

Audio Briefing
0:00 / 0:00

APIにとってコンシューマー主導の契約テストが不可欠な理由

マイクロサービスアーキテクチャは、ライブのステージング環境に依存する脆い統合テストスイートに悩まされることがよくあります。チームAがサービスAのAPIエンドポイントスキーマを変更すると、ダウンストリームのコンシューマーサービスBは、エンドツーエンド統合テストがスキップされたか、デプロイ後に実行されたために、ランタイム中に頻繁に壊れます。

Consumer-Driven Contract Testing Architecture

コンシューマー主導の契約テストは、APIコンシューマーが正確なリクエストとレスポンスの期待値を定義するテストを作成できるようにすることで、従来のAPI検証を覆します。これらの期待値は、標準化されたJSON契約ファイル(Pactファイル)を生成します。その後、プロバイダーサービスは、これらの記録されたリクエストを自身のローカルインスタンスに対してリプレイし、コンシューマーサービスがオンラインである必要なく、そのレスポンスが契約を満たしていることを検証します。

// Example: Pact V4 consumer contract test in Node.js using @pact-foundation/pact
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { fetchUserProfile } from '../src/api-client';

const { like, string, integer } = MatchersV3;

const provider = new PactV4({
  consumer: 'OrderWebClient',
  provider: 'UserService',
  dir: path.resolve(process.cwd(), 'pacts'),
});

describe('User Service Contract Tests', () => {
  it('returns user profile details for a valid user ID', async () => {
    await provider.addInteraction({
      states: [{ description: 'user with ID 101 exists' }],
      uponReceiving: 'a request for user profile 101',
      withRequest: {
        method: 'GET',
        path: '/api/v1/users/101',
        headers: { Accept: 'application/json' },
      },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: integer(101),
          name: string('Jane Doe'),
          email: like('jane.doe@example.com'),
          role: string('admin'),
        },
      },
    });

    await provider.executeTest(async (mockServer) => {
      const user = await fetchUserProfile(mockServer.url, 101);
      expect(user.name).toEqual('Jane Doe');
      expect(user.role).toEqual('admin');
    });
  });
});

このコンシューマー主導のフローを理解することで、契約テストがブラウザのエンドツーエンドテストよりも大幅に高速に実行される理由が明らかになります。コンシューマーテストはローカルのPactモックHTTPサーバーに対して実行されるため、ネットワーク遅延やデータベースシードのオーバーヘッドなしに、テスト実行はミリ秒単位で完了します。

// Client implementation verified by the Pact consumer test
import axios from 'axios';

export interface UserProfile {
  id: number;
  name: string;
  email: string;
  role: string;
}

export async function fetchUserProfile(baseUrl: string, userId: number): Promise<UserProfile> {
  const response = await axios.get(`${baseUrl}/api/v1/users/${userId}`, {
    headers: { Accept: 'application/json' },
  });
  return response.data;
}

生成されたPact JSONファイルには、HTTPリクエストヘッダー、クエリパラメータ、URLパス、および柔軟なボディマッチングルールに関する明示的な仕様が含まれています。like()やinteger()のような柔軟なマッチャーは、ハードコードされた文字列値ではなく、データ型をチェックするようにプロバイダー検証者に指示します。

HTTP RESTインタラクションを超えて、最新のクラウドアーキテクチャは非同期イベントバスを介しても通信します。Pactは、RabbitMQ、Apache Kafka、またはAWS SNSおよびSQSキューを使用するイベント駆動型マイクロサービス向けのメッセージ契約テスト機能を提供します。コンシューマーアプリケーションは、受信イベントメッセージのペイロードスキーマを定義し、プロデューサーがイベントを本番メッセージブローカーに公開する前にイベントペイロード形式を検証できるようにします。

// Asynchronous message contract testing example with Pact
import { MessageConsumerPact, Matchers } from '@pact-foundation/pact';

const messageProvider = new MessageConsumerPact({
  consumer: 'NotificationService',
  provider: 'OrderEventProducer',
  dir: path.resolve(process.cwd(), 'pacts'),
});

describe('Order Created Event Contract', () => {
  it('handles order created domain events correctly', async () => {
    await messageProvider
      .given('order 4004 was created')
      .expectsToReceive('an order created event payload')
      .withContent({
        orderId: Matchers.like('ORD-4004'),
        amount: Matchers.decimal(149.99),
        customerEmail: Matchers.like('customer@example.com'),
      })
      .verify(async (message) => {
        // Verify message handler processes event object cleanly
        const parsed = JSON.parse(message.contents.toString());
        expect(parsed.orderId).toBeDefined();
      });
  });
});
Advertisement

Node.jsでPactコンシューマーの期待値を定義する方法

Pactコンシューマーの期待値を定義するには、ペイロード構造契約を強制しながらプロバイダーに柔軟性を持たせる型安全なマッチャーを使用する必要があります。タイムスタンプ文字列や生成されたIDに対して厳密な等価マッチングを使用すると、データベースの自動インクリメント値が変更されたときにプロバイダー検証が失敗します。

Consumer Expectations Matching Engine

Pact V4は、like()、eachLike()、regex()、およびdatetime()を含む豊富なマッチングDSLを提供します。これらのマッチャーは、コンシューマーテストが特定の短命な値ではなく、構造スキーマを検証することを保証します。

// Advanced Pact V4 consumer test with array and regex matchers
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { fetchOrderHistory } from '../src/order-client';

const { eachLike, string, decimal, regex } = MatchersV3;

const provider = new PactV4({
  consumer: 'DashboardApp',
  provider: 'OrderService',
  dir: path.resolve(process.cwd(), 'pacts'),
});

describe('Order Service List Contract', () => {
  it('returns a list of recent customer orders', async () => {
    await provider.addInteraction({
      states: [{ description: 'customer 502 has existing orders' }],
      uponReceiving: 'a request for customer order history',
      withRequest: {
        method: 'GET',
        path: '/api/v1/customers/502/orders',
        query: { limit: '5' },
      },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          customerId: 502,
          orders: eachLike({
            orderId: regex(/^ORD-\d{6}$/, 'ORD-123456'),
            totalAmount: decimal(99.95),
            status: regex(/^(PENDING|COMPLETED|SHIPPED)$/, 'COMPLETED'),
          }),
        },
      },
    });

    await provider.executeTest(async (mockServer) => {
      const history = await fetchOrderHistory(mockServer.url, 502, 5);
      expect(history.orders.length).toBeGreaterThan(0);
    });
  });
});

eachLike()を使用すると、返されたレスポンスに、指定された構造テンプレートに一致するオブジェクトの配列が含まれていることがアサートされます。このパターンにより、プロバイダーは契約テストのアサーションを壊すことなく、1つのアイテムまたは50のアイテムを返すことができます。

マッチャータイプ使用例検証ルールユースケース
like(val)like('user@example.com')値のデータ型に一致汎用文字列/数値フィールド
eachLike(template)eachLike({ id: 1 })要素がテンプレートに従う配列に一致ページネーションされたリスト応答
regex(pattern, sample)regex(/^\d{4}-\d{2}$/, '2026-07')正規表現文字列マッチングISO日付、UUID、カスタムID
integer(sample)integer(42)整数数値型に一致データベース主キー

ビジネスドメイン要件に基づいて契約を構築することで、過剰な仕様を回避できます。コンシューマーは、実際に読み取りおよび処理する特定のJSONフィールドのみを契約し、プロバイダーが既存の契約を無効にすることなく、応答ペイロードに新しいフィールドを追加できるようにする必要があります。

// Custom header matchers preventing rigid authorization token failures
export const authenticatedHeadersContract = {
  Authorization: regex(/^Bearer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'),
  'Content-Type': 'application/json',
};

CIパイプラインで自動プロバイダー検証を実行する方法

プロバイダー検証は、APIプロデューサーサービスがPact Brokerから公開された契約をダウンロードし、ローカルHTTPサーバーに対して実行するステップです。Pact Verifierは、契約で指定されたリクエストをプロバイダーアプリケーションに送信し、実際のリクエストが期待されるステータスコード、ヘッダー、およびペイロード構造に一致することをアサートします。

Provider Verification Pipeline Architecture

プロバイダー検証を実行するには、Node.jsでプロバイダー状態ハンドラーを設定する必要があります。状態ハンドラーは、検証者によって各インタラクションリクエストが実行される前に、プロバイダーアプリケーションのデータベースまたは内部メモリ状態を準備します。

// Example: Provider verification test script using @pact-foundation/pact Verifier
import { Verifier } from '@pact-foundation/pact';
import { createServer } from '../src/app';
import { Server } from 'http';
import { seedTestDatabase } from '../test/db-helpers';

describe('Pact Provider Verification', () => {
  let server: Server;
  const PORT = 8088;

  beforeAll((done) => {
    const app = createServer();
    server = app.listen(PORT, () => done());
  });

  afterAll((done) => {
    server.close(() => done());
  });

  it('validates contract specs against local provider server', async () => {
    const verifier = new Verifier({
      provider: 'UserService',
      providerBaseUrl: `http://localhost:${PORT}`,
      pactBrokerUrl: process.env.PACT_BROKER_URL || 'https://broker.example.com',
      pactBrokerToken: process.env.PACT_BROKER_TOKEN,
      publishVerificationResult: process.env.CI === 'true',
      providerVersion: process.env.GIT_COMMIT || '1.0.0',
      providerVersionBranch: process.env.GIT_BRANCH || 'main',
      stateHandlers: {
        'user with ID 101 exists': async () => {
          await seedTestDatabase([{ id: 101, name: 'Jane Doe', email: 'jane.doe@example.com', role: 'admin' }]);
          return { description: 'State seeded: user 101 created' };
        },
      },
    });

    const output = await verifier.verifyProvider();
    console.log('Pact verification complete:', output);
  });
});

CI環境でpublishVerificationResult: trueを設定すると、検証の成功または失敗がPact Brokerに公開されます。この検証マトリックス追跡により、チームは特定のコンシューマーのgitコミットとプロバイダーのgitコミット間の互換性を検証できます。

// Express app setup snippet supporting test state hooks
import express from 'express';

export function createServer() {
  const app = express();
  app.use(express.json());

  app.get('/api/v1/users/:id', async (req, res) => {
    const userId = parseInt(req.params.id, 10);
    // Fetch from database seeded by state handler
    const user = await findUserInDatabase(userId);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    return res.json(user);
  });

  return app;
}

async function findUserInDatabase(id: number) {
  // Mock DB query implementation
  return { id, name: 'Jane Doe', email: 'jane.doe@example.com', role: 'admin' };
}

状態ハンドラーをきれいに設定するには、テストの干渉を防ぐためにデータベーストランザクションを分離する必要があります。各プロバイダー検証実行をデータベースロールバックまたは一時的なSQLiteメモリインスタンスでラップすることで、実行順序に関係なくテストが決定論的であることを保証します。

// Transactional database rollback wrapper for provider state handlers
export async function withTestTransaction(callback: () => Promise<void>) {
  const connection = await db.getConnection();
  await connection.beginTransaction();
  try {
    await callback();
  } finally {
    await connection.rollback();
    connection.release();
  }
}

スキーマの破壊的変更とPact Broker Can-I-Deployを処理する方法

Pact Brokerは、契約テストワークフローの中央リポジトリおよび互換性マトリックスエンジンとして機能します。デプロイ前にマトリックスを照会して、サービスバージョンがターゲット環境のすべてのデプロイ済みコンシューマーおよびプロバイダーバージョンと互換性があることを保証するcan-i-deployというCLIユーティリティを提供します。

Pact Broker Can-I-Deploy Matrix

デプロイパイプライン内でcan-i-deployを実行することは、ゲートキーパーとして機能します。プロバイダーチームがアクティブなコンシューマー契約を無効にする破壊的変更を導入した場合、can-i-deployはすぐに失敗し、デプロイの進行を阻止します。

#!/usr/bin/env bash
# CI Script: Executing Pact Broker can-i-deploy check before staging release

set -euo pipefail

SERVICE_NAME="UserService"
VERSION=$(git rev-parse --short HEAD)
ENVIRONMENT="production"

echo "Checking if ${SERVICE_NAME} version ${VERSION} is safe to deploy to ${ENVIRONMENT}..."

npx @pact-foundation/pact-cli broker can-i-deploy \
  --pact-broker-base-url="${PACT_BROKER_URL}" \
  --broker-token="${PACT_BROKER_TOKEN}" \
  --to-environment="${ENVIRONMENT}" \
  --pacticipant="${SERVICE_NAME}" \
  --version="${VERSION}"

echo "Deployment check passed! Service is safe to release."

プロバイダーが破壊的なAPI変更(フィールド名の変更やパラメータ型の変更など)を導入する必要がある場合、契約ワークフローは拡張された非推奨サイクルに従います。まず、コンシューマーは、古いフィールドのフォールバックサポートを維持しながら、新しいフィールドのサポートを宣言するように契約を更新します。

// Versioned migration contract pattern supporting dual schema fields
export function mapUserResponse(data: any) {
  return {
    id: data.id,
    // Accept new field 'fullName' or fall back to legacy 'name'
    displayName: data.fullName || data.name,
    email: data.email,
  };
}

コンシューマーが更新された契約コードを本番環境にデプロイした後、can-i-deployは、アクティブな本番コンシューマーが非推奨のフィールドに依存していないことを検証します。その後初めて、プロバイダーは本番トラフィックを壊すことなくレガシーフィールドを安全に削除できます。

# GitHub Actions workflow for publishing contracts and checking deploy safety
name: Pact Contract Pipeline
on: [push]

jobs:
  contract-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: Run Consumer Contract Tests
        run: npm test -- --grep "Contract"
      - name: Publish Contracts to Pact Broker
        run: |
          npx @pact-foundation/pact-cli broker publish pacts \
            --pact-broker-base-url="${{ secrets.PACT_BROKER_URL }}" \
            --broker-token="${{ secrets.PACT_BROKER_TOKEN }}" \
            --consumer-app-version="${{ github.sha }}" \
            --branch="${{ github.ref_name }}"
      - name: Can I Deploy Check
        run: |
          npx @pact-foundation/pact-cli broker can-i-deploy \
            --pact-broker-base-url="${{ secrets.PACT_BROKER_URL }}" \
            --broker-token="${{ secrets.PACT_BROKER_TOKEN }}" \
            --pacticipant="OrderWebClient" \
            --version="${{ github.sha }}" \
            --to-environment="production"

Pact Brokerで保留中のPactを管理することで、プロバイダーは新しいコンシューマー契約をCI実行に組み込むことができ、プロバイダーのビルドをすぐに壊すことはありません。Pact Verifierは--enable-pendingフラグをサポートしており、これにより未検証の新しい契約はプロバイダーのプルリクエストをブロックするのではなく、保留中としてフラグ付けされます。

Advertisement

大規模なPactメンテナンスのベストプラクティスとは?

数十のマイクロサービスにわたる契約テストスイートを維持するには、契約の粒度、ブローカータグ管理、および状態ハンドラーの分離に関する明確なガイドラインを設定する必要があります。契約テストをHTTPプロトコル境界に厳密に集中させることで、テストスイートの長期的な保守性が保証されます。

契約テストは、内部の単体テストやデータベース層テストに取って代わるべきではありません。Pact契約内で内部検証ルールの網羅的なエッジケースを定義することは避けてください。代わりに、1つの成功契約と最小限のHTTPエラー状態契約(400 Bad Requestや404 Not Foundなど)を定義して、契約検証を高速に保ちます。

// Clean state handler isolation module pattern
export const stateHandlers = {
  'user 101 exists': async () => {
    await db.user.upsert({
      where: { id: 101 },
      update: { name: 'Jane Doe' },
      create: { id: 101, name: 'Jane Doe', email: 'jane@example.com' },
    });
  },
  'user 101 has zero balance': async () => {
    await db.account.update({
      where: { userId: 101 },
      data: { balance: 0.00 },
    });
  },
};

コンシューマー主導の契約テストは、エンジニアリング組織がAPI変更を調整する方法を変革します。Node.jsマイクロサービス間で機械的に強制可能なHTTP契約を確立することで、チームは統合バグを排除し、独立した継続的デプロイパイプラインを維持します。

コンシューマー主導の契約テストを採用することで、チーム間の透明な連携が生まれます。フロントエンドのWeb開発者とバックエンドのAPIエンジニアは、実装コードを記述する前にペイロードスキーマについて事前に合意でき、プルリクエストレビュー中の統合の摩擦を解消します。

Pact Broker内に契約検証Webフックを確立すると、コンシューマーが新しい契約リビジョンを公開するたびにプロバイダー検証ビルドが自動的にトリガーされます。このリアルタイムのフィードバックループにより、APIスキーマの非互換性がコンシューマーのプルリクエストがマージされてから数分で発見されることが保証されます。

Pact Brokerダッシュボードで検証マトリックスレポートを継続的に監視することで、エンジニアリングリーダーはステージング環境と本番環境全体でのクロスサービス依存関係の健全性を可視化できます。

すべてのバックエンドNode.jsマイクロサービスで契約テストテンプレートを標準化することで、新しいマイクロサービスの追加が契約検証パターンを自然に採用することを保証します。API互換性を強制する自動契約検証により、エンジニアリングチームは完全に安心してマイクロサービスアップデートを1日に複数回リリースできます。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement