mcpField
T
testing-strategies

testing-strategies

github.com/rohitg00テスト・品質2.8k188 回インストール25 週間前に更新
リポジトリ

testing-strategies について

ソフトウェアテストを計画・改善する開発者向けのテスト手法集です。振る舞いに基づく構成、契約・スナップショット・プロパティベーステスト、統合テスト、テストダブルを扱います。

主な機能

  1. 01Arrange-Act-Assertのテスト構成
  2. 02Pactによる契約テスト
  3. 03スナップショット・プロパティベーステスト
  4. 04テストコンテナを使った統合テスト
  5. 05振る舞いに絞った最小限のテストダブル

活用シーン

  1. 01観察可能な振る舞いを軸にしたテスト構成
  2. 02サービス間の互換性確認
  3. 03コンテナを使ったデータベース連携のテスト
  4. 04アルゴリズムの不変条件の検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rohitg00/awesome-claude-code-toolkit --skill testing-strategies

インストール後、Claude Code で/testing-strategiesを入力して読み込まれたか確認します。

詳細

テスト戦略

テスト構造(Arrange-Act-Assert)

describe("OrderService", () => {
  describe("createOrder", () => {
    it("creates an order with valid items and returns order ID", async () => {
      const repo = new InMemoryOrderRepository();
      const service = new OrderService(repo);
      const input = { customerId: "c1", items: [{ productId: "p1", quantity: 2 }] };

      const result = await service.createOrder(input);

      expect(result.id).toBeDefined();
      expect(result.status).toBe("pending");
      expect(result.items).toHaveLength(1);
      const saved = await repo.findById(result.id);
      expect(saved).toEqual(result);
    });

    it("rejects order with empty items", async () => {
      const service = new OrderService(new InMemoryOrderRepository());

      await expect(
        service.createOrder({ customerId: "c1", items: [] })
      ).rejects.toThrow("Order must have at least one item");
    });
  });
});

メソッド名ではなく、振る舞いに基づいてテストに名前を付けます。各テストは独立しており、単独で完結している必要があります。

コントラクトテスト(Pact)

import { PactV4 } from "@pact-foundation/pact";

const provider = new PactV4({
  consumer: "OrderService",
  provider: "UserService",
});

describe("UserService contract", () => {
  it("returns user by ID", async () => {
    await provider
      .addInteraction()
      .given("user with id user-1 exists")
      .uponReceiving("a request for user user-1")
      .withRequest("GET", "/api/users/user-1")
      .willRespondWith(200, (builder) => {
        builder.jsonBody({
          id: "user-1",
          name: "Alice",
          email: "[email protected]",
        });
      })
      .executeTest(async (mockServer) => {
        const client = new UserClient(mockServer.url);
        const user = await client.getUser("user-1");
        expect(user.name).toBe("Alice");
      });
  });
});

コントラクトテストでは、両方のサービスを稼働させなくても、コンシューマーの期待がプロバイダーの機能と一致することを検証します。

スナップショットテスト

import { render } from "@testing-library/react";

it("renders the user profile card", () => {
  const { container } = render(
    <UserCard user={{ name: "Alice", email: "[email protected]", role: "admin" }} />
  );

  expect(container).toMatchSnapshot();
});

it("renders the order summary with inline snapshot", () => {
  const summary = formatOrderSummary(mockOrder);

  expect(summary).toMatchInlineSnapshot(`
    "Order #123
    Items: 3
    Total: $45.99
    Status: Pending"
  `);
});

出力が小さい場合は、インラインスナップショットを使用します。コードレビューでは、スナップショットの差分を慎重に確認します。

プロパティベーステスト

import fc from "fast-check";

describe("sortUsers", () => {
  it("always returns the same number of elements", () => {
    fc.assert(
      fc.property(
        fc.array(fc.record({ name: fc.string(), age: fc.nat(120) })),
        (users) => {
          const sorted = sortUsers(users, "name");
          return sorted.length === users.length;
        }
      )
    );
  });

  it("produces a sorted result for any input", () => {
    fc.assert(
      fc.property(
        fc.array(fc.record({ name: fc.string(), age: fc.nat(120) })),
        (users) => {
          const sorted = sortUsers(users, "age");
          for (let i = 1; i < sorted.length; i++) {
            if (sorted[i].age < sorted[i - 1].age) return false;
          }
          return true;
        }
      )
    );
  });
});

テストコンテナを使った統合テスト

import { PostgreSqlContainer } from "@testcontainers/postgresql";

let container: any;
let db: Database;

beforeAll(async () => {
  container = await new PostgreSqlContainer("postgres:16").start();
  db = await createDatabase(container.getConnectionUri());
  await db.migrate();
}, 60000);

afterAll(async () => {
  await db.close();
  await container.stop();
});

it("creates and retrieves a user", async () => {
  const user = await db.user.create({ name: "Alice", email: "[email protected]" });
  const found = await db.user.findById(user.id);
  expect(found).toEqual(user);
});

テストダブル

function createMockEmailService(): EmailService {
  const sent: Array<{ to: string; subject: string }> = [];
  return {
    send: async (to, subject, body) => { sent.push({ to, subject }); },
    getSent: () => sent,
  };
}

const emailService = createMockEmailService();
const service = new NotificationService(emailService);
await service.notifyUser("user-1", "Welcome");
expect(emailService.getSent()).toHaveLength(1);
expect(emailService.getSent()[0].subject).toBe("Welcome");

アンチパターン

  • 振る舞いではなく実装の詳細をテストする
  • テスト間で変更可能な状態を共有する(beforeEach でリセットしない)
  • 実行順序に依存するテストを書く
  • 統合テストで実際の依存関係を使わず、すべてをモック化する
  • 不安定なテストの根本原因を修正せず、放置する
  • エッジケースを見落とし、単純なゲッター/セッターをテストする

チェックリスト

  • テストはメソッドやファイルではなく、振る舞いごとに整理されている
  • 各テストは Arrange-Act-Assert の構造に従っている
  • コントラクトテストでサービス間の API 互換性を検証している
  • スナップショットテストはコードレビューで確認されている(盲目的に更新していない)
  • プロパティベーステストで、アルゴリズムを含むコードの不変条件を網羅している
  • 統合テストでは実際の依存関係にテストコンテナを使用している
  • テストダブルは最小限で、振る舞いに焦点を当てている
  • CI で不安定なテストが検出された場合、失敗する

コメント