testing-strategies について
ソフトウェアテストを計画・改善する開発者向けのテスト手法集です。振る舞いに基づく構成、契約・スナップショット・プロパティベーステスト、統合テスト、テストダブルを扱います。
主な機能
- 01Arrange-Act-Assertのテスト構成
- 02Pactによる契約テスト
- 03スナップショット・プロパティベーステスト
- 04テストコンテナを使った統合テスト
- 05振る舞いに絞った最小限のテストダブル
活用シーン
- 01観察可能な振る舞いを軸にしたテスト構成
- 02サービス間の互換性確認
- 03コンテナを使ったデータベース連携のテスト
- 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 で不安定なテストが検出された場合、失敗する
コメント
Markdown 対応