Agent Skills by ALSEL
汎用ソフトウェア開発⭐ リポ 9品質スコア 80/100

contract-testing

サービス間のAPI互換性を検証するコントラクトテストの構築・改善の際に使用します。Pact(コンシューマー駆動型コントラクト)、PactFlow(双方向コントラクト)、Spring Cloud Contract、Pact Broker、can-i-deployの各ツールと、コントラクトテスト手法の選定戦略に対応しています。 使用対象:Pact、PactFlow、Spring Cloud Contract、コンシューマー駆動型コントラクト、双方向コントラクト、API互換性検証、can-i-deploy、Pact Broker 使用非対象:ユーザーフロー全体のテスト(e2e-testingを使用)、API機能テスト(api-testingを使用)、ユニットテスト(unit-testingを使用)

description の原文を見る

Use when setting up or improving contract tests that verify API compatibility between services. Covers Pact (consumer-driven contracts), PactFlow (bi-directional contracts), Spring Cloud Contract, Pact Broker, can-i-deploy, and strategies for choosing between contract testing approaches. USE FOR: Pact, PactFlow, Spring Cloud Contract, consumer-driven contracts, bi-directional contracts, API compatibility verification, can-i-deploy, Pact Broker DO NOT USE FOR: full user flow tests (use e2e-testing), API functional testing (use api-testing), unit testing (use unit-testing)

SKILL.md 本文

コントラクトテスト — サービス間の API 互換性の検証

概要

コントラクトテストは、2 つのサービス(コンシューマープロバイダー)が両方とも同時に実行されていなくても正しく通信できることを検証します。全体の統合テストではなく、各側がコントラクトに対して検証します。コントラクトは、期待されるリクエスト/レスポンスのやり取りの正式な仕様です。

E2E テストの代わりにコントラクトテストを使用する場合: マイクロサービスアーキテクチャでは E2E テストが遅くなり、不安定になり、数十のサービスにわたって維持するのが高くつきます。

コントラクトテストのアプローチ

アプローチ仕組み最適な用途
コンシューマー主導コンシューマーがコントラクトを作成し、プロバイダーが検証最も一般的 — コンシューマーが必要な内容を認識
プロバイダー主導プロバイダーが API 仕様を公開し、コンシューマーが対して検証多くの未知のコンシューマーがいる API
双方向両方とも共有の仕様(OpenAPI など)に対して検証既に OpenAPI 仕様を保守しているチーム

コンシューマー主導コントラクトテスト(Pact ワークフロー)

Consumer                    Pact Broker                  Provider
   │                            │                            │
   ├─ 1. Write consumer test ──►│                            │
   │     (generates pact file)  │                            │
   │                            │                            │
   ├─ 2. Publish pact ─────────►│                            │
   │                            │                            │
   │                            │◄── 3. Fetch pact ──────────┤
   │                            │                            │
   │                            │    4. Provider verifies ───►│
   │                            │       against pact          │
   │                            │                            │
   │                            │◄── 5. Publish result ──────┤
   │                            │                            │
   ├── 6. can-i-deploy? ──────►│                            │
   │     (checks compatibility) │                            │
   │                            │                            │

Pact

Pact は最も広く使用されているコントラクトテストフレームワークで、多くの言語にわたってコンシューマー主導のコントラクトをサポートしています。

JavaScript — コンシューマーテスト

// consumer/user-client.pact.test.ts
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import { UserClient } from './user-client';

const { like, eachLike, regex, integer, string } = MatchersV3;

const provider = new PactV4({
  consumer: 'UserWebApp',
  provider: 'UserService',
  logLevel: 'warn',
});

describe('UserClient', () => {
  it('should fetch a user by ID', async () => {
    // Define the expected interaction
    await provider
      .addInteraction()
      .given('user 123 exists')
      .uponReceiving('a request for user 123')
      .withRequest('GET', '/api/users/123', (builder) => {
        builder.headers({ Accept: 'application/json' });
      })
      .willRespondWith(200, (builder) => {
        builder
          .headers({ 'Content-Type': 'application/json' })
          .jsonBody({
            id: integer(123),
            name: string('Alice'),
            email: regex('alice@example.com', '^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$'),
            role: string('admin'),
          });
      })
      .executeTest(async (mockServer) => {
        // Point the client at the mock server
        const client = new UserClient(mockServer.url);

        // Call the client — it talks to the Pact mock
        const user = await client.getUser(123);

        // Verify the client handles the response correctly
        expect(user.id).toBe(123);
        expect(user.name).toBe('Alice');
        expect(user.email).toBe('alice@example.com');
      });
  });

  it('should fetch a list of users', async () => {
    await provider
      .addInteraction()
      .given('users exist')
      .uponReceiving('a request for all users')
      .withRequest('GET', '/api/users', (builder) => {
        builder.headers({ Accept: 'application/json' });
      })
      .willRespondWith(200, (builder) => {
        builder
          .headers({ 'Content-Type': 'application/json' })
          .jsonBody(
            eachLike({
              id: integer(1),
              name: string('Alice'),
              email: string('alice@example.com'),
            })
          );
      })
      .executeTest(async (mockServer) => {
        const client = new UserClient(mockServer.url);
        const users = await client.listUsers();

        expect(users).toHaveLength(1);
        expect(users[0].name).toBe('Alice');
      });
  });

  it('should return 404 for non-existent user', async () => {
    await provider
      .addInteraction()
      .given('user 999 does not exist')
      .uponReceiving('a request for user 999')
      .withRequest('GET', '/api/users/999')
      .willRespondWith(404, (builder) => {
        builder.jsonBody({
          error: string('User not found'),
        });
      })
      .executeTest(async (mockServer) => {
        const client = new UserClient(mockServer.url);

        await expect(client.getUser(999)).rejects.toThrow('User not found');
      });
  });
});

JavaScript — プロバイダー検証

// provider/pact-verification.test.ts
import { Verifier } from '@pact-foundation/pact';
import { createApp } from '../src/app';

describe('Provider Verification', () => {
  let server: any;

  beforeAll(async () => {
    const app = await createApp();
    server = app.listen(0);
  });

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

  it('should validate the expectations of UserWebApp', async () => {
    const port = server.address().port;

    await new Verifier({
      providerBaseUrl: `http://localhost:${port}`,
      provider: 'UserService',
      pactBrokerUrl: process.env.PACT_BROKER_URL || 'http://localhost:9292',
      pactBrokerToken: process.env.PACT_BROKER_TOKEN,
      publishVerificationResult: process.env.CI === 'true',
      providerVersion: process.env.GIT_SHA || '1.0.0',
      providerVersionBranch: process.env.GIT_BRANCH || 'main',
      stateHandlers: {
        'user 123 exists': async () => {
          // Seed the database with user 123
          await seedUser({ id: 123, name: 'Alice', email: 'alice@example.com', role: 'admin' });
        },
        'user 999 does not exist': async () => {
          // Ensure user 999 does not exist
          await deleteUser(999);
        },
        'users exist': async () => {
          await seedUser({ id: 1, name: 'Alice', email: 'alice@example.com' });
        },
      },
    }).verifyProvider();
  });
});

C# — コンシューマーテスト(PactNet)

using PactNet;
using PactNet.Matchers;
using Xunit;

public class UserClientPactTests
{
    private readonly IPactBuilderV4 _pactBuilder;

    public UserClientPactTests()
    {
        var pact = Pact.V4("UserWebApp", "UserService", new PactConfig
        {
            PactDir = "../../../pacts",
            LogLevel = PactLogLevel.Warning,
        });
        _pactBuilder = pact.WithHttpInteractions();
    }

    [Fact]
    public async Task GetUser_WhenUserExists_ReturnsUser()
    {
        _pactBuilder
            .UponReceiving("a request for user 123")
            .Given("user 123 exists")
            .WithRequest(HttpMethod.Get, "/api/users/123")
            .WithHeader("Accept", "application/json")
            .WillRespond()
            .WithStatus(HttpStatusCode.OK)
            .WithHeader("Content-Type", "application/json")
            .WithJsonBody(new
            {
                id = Match.Integer(123),
                name = Match.Type("Alice"),
                email = Match.Regex("alice@example.com", @"^[\w.-]+@[\w.-]+\.[a-z]{2,}$"),
                role = Match.Type("admin"),
            });

        await _pactBuilder.VerifyAsync(async ctx =>
        {
            var client = new UserClient(ctx.MockServerUri.ToString());

            var user = await client.GetUserAsync(123);

            Assert.Equal(123, user.Id);
            Assert.Equal("Alice", user.Name);
        });
    }

    [Fact]
    public async Task GetUser_WhenUserDoesNotExist_Returns404()
    {
        _pactBuilder
            .UponReceiving("a request for non-existent user")
            .Given("user 999 does not exist")
            .WithRequest(HttpMethod.Get, "/api/users/999")
            .WillRespond()
            .WithStatus(HttpStatusCode.NotFound)
            .WithJsonBody(new
            {
                error = Match.Type("User not found"),
            });

        await _pactBuilder.VerifyAsync(async ctx =>
        {
            var client = new UserClient(ctx.MockServerUri.ToString());

            await Assert.ThrowsAsync<UserNotFoundException>(
                () => client.GetUserAsync(999));
        });
    }
}

C# — プロバイダー検証(PactNet)

using PactNet;
using PactNet.Verifier;
using Xunit;

public class ProviderPactTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly CustomWebApplicationFactory _factory;

    public ProviderPactTests(CustomWebApplicationFactory factory)
    {
        _factory = factory;
    }

    [Fact]
    public void VerifyPacts()
    {
        var config = new PactVerifierConfig
        {
            LogLevel = PactLogLevel.Warning,
        };

        using var server = _factory.Server;

        var verifier = new PactVerifier("UserService", config);

        verifier
            .WithHttpEndpoint(server.BaseAddress)
            .WithPactBrokerSource(new Uri("http://localhost:9292"), options =>
            {
                options
                    .ConsumerVersionSelectors(
                        new ConsumerVersionSelector { MainBranch = true },
                        new ConsumerVersionSelector { DeployedOrReleased = true }
                    )
                    .PublishResults(
                        Environment.GetEnvironmentVariable("GIT_SHA") ?? "1.0.0",
                        options => options.ProviderBranch(
                            Environment.GetEnvironmentVariable("GIT_BRANCH") ?? "main"));
            })
            .WithProviderStateUrl(new Uri(server.BaseAddress, "/provider-states"))
            .Verify();
    }
}

Java — コンシューマーテスト(Pact JVM)

import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.core.model.V4Pact;
import au.com.dius.pact.core.model.annotations.Pact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "UserService")
class UserClientPactTest {

    @Pact(consumer = "UserWebApp")
    V4Pact getUserPact(PactDslWithProvider builder) {
        return builder
            .given("user 123 exists")
            .uponReceiving("a request for user 123")
            .path("/api/users/123")
            .method("GET")
            .headers("Accept", "application/json")
            .willRespondWith()
            .status(200)
            .headers(Map.of("Content-Type", "application/json"))
            .body(newJsonBody(body -> {
                body.integerType("id", 123);
                body.stringType("name", "Alice");
                body.stringMatcher("email", "^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$", "alice@example.com");
                body.stringType("role", "admin");
            }).build())
            .toPact(V4Pact.class);
    }

    @Test
    @PactTestFor(pactMethod = "getUserPact")
    void getUser_whenUserExists_returnsUser(MockServer mockServer) {
        var client = new UserClient(mockServer.getUrl());

        var user = client.getUser(123);

        assertEquals(123, user.getId());
        assertEquals("Alice", user.getName());
    }
}

Pact Broker と can-i-deploy

Pact Broker

Pact Broker はコントラクトの中央リポジトリで、チーム間でのコントラクトテストワークフロー全体を実現します。

# Pact Broker をローカルで実行
docker run -d \
  --name pact-broker \
  -p 9292:9292 \
  -e PACT_BROKER_DATABASE_URL=sqlite:////tmp/pact_broker.sqlite3 \
  pactfoundation/pact-broker

# Pact をブローカーに公開
npx pact-broker publish ./pacts \
  --consumer-app-version=$(git rev-parse --short HEAD) \
  --branch=$(git branch --show-current) \
  --broker-base-url=http://localhost:9292

# 環境用にバージョンをタグ付け
npx pact-broker create-version-tag \
  --pacticipant=UserWebApp \
  --version=$(本git rev-parse --short HEAD) \
  --tag=main

can-i-deploy

can-i-deploy は重要な安全チェックです。ターゲット環境にデプロイされているすべてのものとサービスバージョンが互換性があるかどうかを教えてくれます。

# コンシューマーが本番環境にデプロイできるかを確認
npx pact-broker can-i-deploy \
  --pacticipant=UserWebApp \
  --version=$(git rev-parse --short HEAD) \
  --to-environment=production \
  --broker-base-url=http://localhost:9292

# プロバイダーがデプロイできるかを確認
npx pact-broker can-i-deploy \
  --pacticipant=UserService \
  --version=$(git rev-parse --short HEAD) \
  --to-environment=production \
  --broker-base-url=http://localhost:9292

# デプロイを記録
npx pact-broker record-deployment \
  --pacticipant=UserService \
  --version=$(git rev-parse --short HEAD) \
  --environment=production \
  --broker-base-url=http://localhost:9292

PactFlow(双方向コントラクトテスト)

PactFlow は Pact を双方向コントラクトテストで拡張します — プロバイダーが OpenAPI 仕様を公開し、コンシューマーが Pact コントラクトを公開します。PactFlow は 2 つの間の互換性を検証します。

ワークフロー

Consumer                     PactFlow                     Provider
   │                            │                            │
   ├─ 1. Publish pact ─────────►│                            │
   │                            │                            │
   │                            │◄── 2. Publish OpenAPI ─────┤
   │                            │                            │
   │                            │── 3. Cross-validate ──────►│
   │                            │   (pact vs OpenAPI spec)   │
   │                            │                            │
   ├── 4. can-i-deploy? ──────►│                            │
# プロバイダーが OpenAPI 仕様を公開
npx pactflow publish-provider-contract \
  openapi.yaml \
  --provider=UserService \
  --provider-app-version=$(git rev-parse --short HEAD) \
  --branch=$(git branch --show-current) \
  --content-type=application/yaml \
  --verification-exit-code=0 \
  --verification-results=oas-report.txt \
  --verification-results-content-type=text/plain \
  --broker-base-url=https://your-org.pactflow.io \
  --broker-token=$PACTFLOW_TOKEN

Spring Cloud Contract

Spring Cloud Contract は Groovy DSL または Kotlin DSL を使用して JVM アプリケーション向けのコントラクトテストを提供します。

Groovy DSL コントラクト

// src/test/resources/contracts/user/get_user.groovy
package contracts.user

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "should return user by ID"
    request {
        method GET()
        url "/api/users/123"
        headers {
            accept(applicationJson())
        }
    }
    response {
        status OK()
        headers {
            contentType(applicationJson())
        }
        body([
            id: 123,
            name: "Alice",
            email: "alice@example.com",
            role: "admin"
        ])
    }
}

Kotlin DSL コントラクト

// src/test/resources/contracts/user/get_user.kts
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    description = "should return user by ID"
    request {
        method = GET
        url = url("/api/users/123")
        headers {
            accept = APPLICATION_JSON
        }
    }
    response {
        status = OK
        headers {
            contentType = APPLICATION_JSON
        }
        body = body(mapOf(
            "id" to 123,
            "name" to "Alice",
            "email" to "alice@example.com",
            "role" to "admin"
        ))
    }
}

プロバイダーベーステスト

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public abstract class BaseContractTest {

    @Autowired
    private UserController userController;

    @BeforeEach
    void setUp() {
        RestAssuredMockMvc.standaloneSetup(userController);
        // Seed test data
        seedUser(new User(123, "Alice", "alice@example.com", "admin"));
    }
}
# コントラクトテストを生成して実行
./gradlew contractTest

# スタブを Maven リポジトリに公開
./gradlew publishStubsPublicationToMavenLocal

# コンシューマーがスタブを使用
./gradlew test -Dstubrunner.ids=com.example:user-service:+:stubs:8080

コントラクトテスト vs E2E テストを使用する場合

シナリオコントラクトテストを使用E2E テストを使用
マイクロサービス API 境界はいいいえ(サービスが多い)
破壊的な API 変更検出はい部分的(遅く、不安定)
全体的なユーザーフローの検証いいえはい
デプロイ安全チェックはい(can-i-deploy)いいえ(CI 用に遅い)
UI 動作の検証いいえはい
プロバイダー API の進化はいいいえ
フロントエンド + バックエンド統合場合によって(API レイヤー)はい(ユーザーに見える動作)
サードパーティ API 互換性はい(コントラクトを提供する場合)いいえ(制御なし)

判定チェックリスト

  • コントラクトテストを使用する場合: API 経由で通信する複数のサービスがあり、高速で信頼性の高い互換性チェックが必要な場合。
  • E2E テストを使用する場合: 実際の UI を通じたユーザー体験全体を検証する必要がある場合。
  • 両方を使用する場合: マイクロサービスアーキテクチャと Web フロントエンドがある場合 — サービス境界用のコントラクトテスト、重要なユーザーフロー用の E2E テスト。

CI 統合

GitHub Actions — Pact ワークフロー

name: Contract Tests

on: [push, pull_request]

jobs:
  consumer-contract-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - run: npm ci

      - name: Run Consumer Contract Tests
        run: npx vitest run --project contracts

      - name: Publish Pacts
        if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request'
        run: |
          npx pact-broker publish ./pacts \
            --consumer-app-version=${{ github.sha }} \
            --branch=${{ github.head_ref || github.ref_name }} \
            --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
            --broker-token=${{ secrets.PACT_BROKER_TOKEN }}

      - name: Can I Deploy?
        if: github.ref == 'refs/heads/main'
        run: |
          npx pact-broker can-i-deploy \
            --pacticipant=UserWebApp \
            --version=${{ github.sha }} \
            --to-environment=production \
            --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
            --broker-token=${{ secrets.PACT_BROKER_TOKEN }}

  provider-verification:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - run: npm ci

      - name: Verify Provider Against Pacts
        run: npx vitest run --project provider-verification
        env:
          PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
          GIT_SHA: ${{ github.sha }}
          GIT_BRANCH: ${{ github.head_ref || github.ref_name }}
          CI: true

      - name: Can I Deploy?
        if: github.ref == 'refs/heads/main'
        run: |
          npx pact-broker can-i-deploy \
            --pacticipant=UserService \
            --version=${{ github.sha }} \
            --to-environment=production \
            --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
            --broker-token=${{ secrets.PACT_BROKER_TOKEN }}

クロスプラットフォームツール一覧

ツール対応言語コントラクト形式
PactJS/TS、Java、C#、Python、Go、Ruby、Rustコンシューマー主導
PactFlow任意(OpenAPI 経由)双方向
Spring Cloud ContractJava、Kotlin、Groovyプロバイダー主導 / コンシューマースタブ
Pact Broker言語非依存コントラクトリポジトリ
can-i-deployCLI ツールデプロイ安全ゲート

ベストプラクティス

  • コンシューマー主導のコントラクト(Pact)で開始します — コンシューマーが必要な内容を最も良く知っています。
  • Pact Broker を使用してチーム間でコントラクトを共有し、検証ステータスを追跡します。
  • 本番環境にデプロイする前に常に can-i-deploy を実行します — これはセーフティネットです。
  • プロバイダー状態(given(...))を使用してプロバイダー内の特定のテストシナリオを設定します。
  • 正確な値ではなく Pact マッチャー(likeeachLikeregex)を使用します — コントラクトは柔軟であるべきです。
  • コンシューマーが実際に使用する最小限のフィールドセットをテストします。全体の API レスポンスではなく。
  • git SHA とブランチでコントラクトのバージョン管理を行い、トレーサビリティを確保します。
  • すべてのコミットでコンシューマーコントラクトテストを実行します — 実サービスが不要なので高速です。
  • すべての PR でプロバイダー検証を実行します — マージ前に破壊的な変更を検出します。
  • 既に OpenAPI 仕様を保守している場合は、双方向コントラクトテスト(PactFlow)を使用します。
  • コントラクトテストを機能的な API テストの代替として使用しないでください — これらは形状を検証するもので、ビジネスロジックは検証しません。
  • 環境ごとに何が実行されているかを can-i-deploy が認識するように、Pact Broker にデプロイを記録します。

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
Tyler-R-Kendrick
リポジトリ
Tyler-R-Kendrick/agent-skills
ライセンス
MIT
最終更新
2026/2/11

Source: https://github.com/Tyler-R-Kendrick/agent-skills / ライセンス: MIT

本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: Tyler-R-Kendrick · Tyler-R-Kendrick/agent-skills · ライセンス: MIT