testing について
testing は、振る舞いを重視したソフトウェアテストの計画と作成を支援するスキルです。テスト範囲、ミューテーションを意識したアサーション、主張に適した公開インターフェースでの検証を扱います。
主な機能
- 01実装ではなく振る舞いをテスト
- 02影響を受けるテスト範囲の選択
- 03想定されるミュータントに備えたアサーション
- 04適切な公開インターフェースを通じたテスト
- 05テストだけを目的とした関数抽出の回避
活用シーン
- 01変更した振る舞いのテスト計画
- 02境界値と正規化ケースの確認
- 03実際のインターフェースを通じたユーザージャーニーのテスト
- 04コード変更後の適切なテスト範囲の選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill testingインストール後、Claude Code で/testingを入力して読み込まれたか確認します。
詳細
テストパターン
ミューテーション分析でテストの有効性を検証するには、mutation-testing スキルを読み込んでください。テストを計画・作成する際にはそのミューテーションルールを使用し、自動ミューテーションハーネスの実行はフェーズ終了時の PR 準備状況チェックポイントまで延期してください。Dave Farley の特性に照らしてテスト品質を評価するには、test-design-reviewer スキルを読み込んでください。
基本原則
実装ではなく、振る舞いをテストしてください。 カバレッジは未実行のコードを見つけるのに役立ちます。しきい値はリポジトリごとに定めるものであり、割合は意味のあるアサーションに取って代わるものではありません。
例: テスト対象の契約が processPayment() であれば、カバレッジの数値を上げるためだけに内部バリデーターをテストするのではなく、processPayment() を通じて検証を実行してください。
高速で関連性の高いテスト実行
tdd スキルの標準的な高速フィードバック、ウォッチャーのライフサイクル、プロセスのクリーンアップ、モノレポ、最終ゲート、リポジトリの権威に関するポリシーに従ってください。Vitest のシードのセマンティクスと再利用可能なコマンドの検証については、resources/vitest-watch-feedback.md を読んでください。
RED では、まず正確なセレクターを使います。GREEN/REFACTOR では、動作が検証済みのリポジトリ管理ウォッチャーを優先し、次にランナー/オーケストレーターから導出した影響範囲または影響を受ける対象のワンショット実行を選びます。リポジトリ管理コマンドは、ランタイムグラフのシードを作成するために、関連する完全なティアを意図的に一度実行してもかまいません。実行を続ける対象を手作業でより小さく絞ったり、テストがゼロ件であることを根拠として認めたり、ウォッチャーを残したままにしたり、完全な非ウォッチの PR ゲートの代わりにウォッチモードを使ったりしないでください。
フラグを推測するのではなく、プロジェクトのスクリプトとインストール済みランナーのヘルプを確認してください。代表的なランナー固有の選択肢は次のとおりです。
優先順位:
- 動作検証済みのリポジトリ管理ウォッチ/影響範囲コマンド。
- インストール済みランナーとリポジトリの設定でウォッチ動作が検証済みの場合、ネイティブの VCS 対応選択。
- 変更されたソースの完全なリストを機械的に導出したうえで、ネイティブの依存関係対応選択。
- 推移的な依存先を含む、ランナー/オーケストレーターが選択した影響を受けるパッケージ/プロジェクト。
- RED の検証またはデバッグに限り、テストファイル/名前を正確に指定し、その後 GREEN ではより広い影響範囲のいずれかを実行。
| ランナー | GREEN/REFACTOR の影響範囲 | RED/デバッグ専用セレクター |
|---|---|---|
| Vitest | まずリポジトリ管理ウォッチャーを使います。それ以外の場合、tdd スキルのバージョン/設定の検証条件を満たす場合に限り、vitest --changed <real-base> --watch を使います。ワンショット実行には --run を使います。vitest related <all-changed-source-paths> を使えるのは、そのリストが機械的に導出されている場合に限ります | vitest run <test-file> -t <name> または正確なファイルを指定したウォッチ |
| Jest | jest --watch を使い、タスクが複数のコミットにまたがる場合は --changedSince=<base> を追加します。ワンショット実行では、ウォッチを無効にして --onlyChanged、--changedSince=<base>、または --findRelatedTests <all-changed-source-paths> を使います | jest --runTestsByPath <test-file> -t <name> |
| pytest | 既存のリポジトリの影響範囲/ウォッチタスクを使います。ない場合は、依存関係の影響が不確かなときに対象パッケージ/スイート全体と既知の利用側を実行し、範囲を広げます | pytest path/to/test_file.py::test_name または pytest -k <name> <path> |
| Playwright Test | リポジトリのスクリプト、またはランナーが選択する影響範囲のワンショット実行として playwright test --only-changed[=<real-base>] を優先します。機械的に導出した影響を受けるプロジェクトの完全なセットは、プロジェクトフィルターで指定できます。テストからインポートされないランタイムアプリのコード、動的/非インポート依存関係、または共有されるグローバル入力については、リポジトリで対応付けられた影響を受けるジャーニー/プロジェクトのセットを使い、不確かな場合は範囲を広げます | RED の検証または既知の失敗の修正に限り、ファイル、--grep、--last-failed、または手作業で選んだプロジェクトフィルターを使用 |
| Go/Rust/JVM | リポジトリに既存の影響範囲/ウォッチタスクがあればそれを使います。なければ、推移的な利用側を含む依存関係グラフから導出したパッケージ/モジュールのセットを使います | RED の検証またはデバッグに限り、パッケージ/クラス/テスト名を正確に指定 |
ランナーに依存関係グラフがない場合、RED/デバッグに限って正確なセレクターを使います。GREEN/REFACTOR では、動作テストに加えて既知の利用側と、対象のスイート/プロジェクトの完全なセットを実行し、影響が不確かな場合は範囲を広げてください。「変更されたテストファイル」を「関連するすべてのテスト」と取り違えないでください。対象となるのは、テストファイル自体が変更されたかどうかにかかわらず、変更された動作の影響を受けるテストです。
Vitest の related は、結果が空でも暗黙的に合格します。少なくとも想定されるテストが 1 件実行されることを確認してください。Vitest の --standalone は TDD の代替ではありません。標準的な TDD ポリシーに従ってください。
ミューテーションを考慮したテスト計画
テストを計画または作成するときは、テスト対象のコード(新規コード、変更されたコード、または初めてテスト対象にする既存コード)を、mutation-testing スキルの resources/mutator-rules.md リソースにあるミューテータールールと照らし合わせて自動的に確認します。現実的なミュータントによって動作が変わった場合に失敗するテストが、優れたテストです。
最初のアサーションを書く前に、これからテスト対象にするコードについて、次の2つのリストを作成します。壊れたルールがあってもテストスイートが成功しがちな箇所です。
- 定数との比較すべて。 しきい値に対する各
>、>=、<、<=または===について、しきい値の正確な値を特定し、その直下、ちょうどその値、直上をテストします。しきい値を大きく超える値では、>と>=の違いを判別できません。しきい値ちょうどのテストだけが判別できます。 - 値の入力時または出力時に正規化や変換を行うすべての呼び出し。 大文字・小文字の統一、トリミング、並べ替え、重複の排除、丸め、欠落フィールドのデフォルト値設定は、それぞれ呼び出し元から観察できる動作であり、削除されればその動作はなくなります。呼び出しが削除された場合に結果が変わる入力をそれぞれ選びます。大文字・小文字を統一して検索するキーなら大文字・小文字を変えたキー、トリミングする入力なら余分な空白を含む入力、出力を順序付けるなら順不同の入力を選び、結果をアサートします。
コードから確認できる動作を押さえていなければ、その動作はテストされていません。依頼で明示されていなくても、また該当行がカバレッジに含まれていても同じです。
テスト対象のコードに条件分岐、算術、等価性、ブール論理、配列・文字列操作、オプショナルチェーン、または意味のある副作用が含まれる場合は、そのリソースを読み込みます。Stryker の実行前に、残存しそうなミュータントの特定に活用します。
確認によって明らかな不足が見つかったら、すぐに動作テストを追加または強化します。不足の判断にプロダクトやドメインに関する判断が必要な場合は、利用可能であればホストの構造化された質問機能を使い、そうでなければ簡潔な質問を1つだけプレーンテキストで尋ねます。具体的な選択肢を示し、想定されるミュータントとトレードオフを説明します。
質問の例:
The bulk path uses `items.length >= 100`, but current tests only cover `150`.
Should the exact `100` boundary use the bulk path?
- Yes: add a boundary test for `100`
- No: change/confirm the rule as `items.length > 100`
- Unspecified: document the behavior as intentionally not guaranteed
アサーション、境界値、分岐、副作用の確認が明らかに欠けているだけの場合は、質問しないでください。直接修正します。
テスト対象の公開インターフェースを通じてテストする
実装の詳細をテストしてはいけません。テスト対象の公開インターフェースを通じて、テストの主張が示すレイヤーで動作をテストします。
この方法が重要な理由:
- リファクタリング後もテストが有効なまま保たれます
- テストが意図した動作を記録します
- 実際のバグを検出し、実装の変更は検出しません
「公開 API」は「HTTP API」を意味するわけではありません。各レイヤーにはそれぞれ独自の公開インターフェースがあり、どのインターフェースが根拠を担うかは、テスト対象の主張によって決まります。
| テスト対象の主張 | 根拠を担う公開インターフェース |
|---|---|
| ドメイン/アプリケーションの動作 | エクスポートされたドメイン/アプリケーションの操作 |
| HTTP API の契約 | HTTP クライアントと、文書化されたリクエスト/レスポンスの契約 |
| コンポーネントの動作 | レンダリングされたコンポーネントの DOM と公開 props/イベント |
| ブラウザー/フロントエンドの動作 | ナビゲーション、アクセシブルな UI、ブラウザーのライフサイクル、ブラウザーから観察したネットワーク |
| ユーザージャーニー | 実際のジャーニー全体にわたる、アクセシブルなユーザー操作とユーザーから見える結果 |
HTTP エンドポイントが公開されていても、ブラウザーに関する主張をテストするには不適切なインターフェースである場合があります。「ユーザーが X を作成する」というテストがエンドポイントを直接呼び出すと、UI ハンドラー、Cookie ポリシー、CSRF/Fetch Metadata のチェック、リダイレクト、読み込み/エラー状態、レンダリングを経由せずに HTTP 契約を証明することになります。そのため、これらのいずれかが壊れてもテストは成功したままです。テスト名、コメント、CI のステップ名、ドキュメント、PR の説明には、実際に証明できる範囲で最も限定的な根拠を明記してください。E2E の根拠モデル、リクエストの観察、直接トランスポートの監査については、front-end-testing スキルの resources/playwright-e2e.md を読み込みます。
例
❌ 誤り — 実装をテストする:
// ❌ Testing HOW (implementation detail)
it('should call validateAmount', () => {
const spy = vi.spyOn(validator, 'validateAmount');
processPayment(payment);
expect(spy).toHaveBeenCalled(); // Tests HOW, not WHAT
});
// ❌ Testing private methods
it('should validate CVV format', () => {
const result = validator._validateCVV('123'); // Private method!
expect(result).toBe(true);
});
// ❌ Testing internal state
it('should set isValidated flag', () => {
processPayment(payment);
expect(processor.isValidated).toBe(true); // Internal state
});
✅ 正しい — 公開 API を通して動作をテストする:
it('should reject negative amounts', () => {
const payment = getMockPayment({ amountMinorUnits: -100, currency: 'GBP' });
const result = processPayment(payment);
expect(result).toEqual({ success: false, error: expect.stringContaining('Amount must be positive') });
});
it('should reject invalid CVV', () => {
const payment = getMockPayment({ cvv: '12' }); // Only 2 digits
const result = processPayment(payment);
expect(result).toEqual({ success: false, error: expect.stringContaining('Invalid CVV') });
});
it('should process valid payments', () => {
const payment = getMockPayment({ amountMinorUnits: 10_000, currency: 'GBP', cvv: '123' });
const result = processPayment(payment);
expect(result).toEqual({ success: true, data: { transactionId: expect.any(String) } });
});
別途 expect(result.success) を行ってから result.error/result.data にアクセスするのではなく、Result 値全体をアサートしてください。expect(...).toBe(false) では判別共用体の型が絞り込まれないため、型が絞り込まれていない Result のメンバーにアクセスすると、厳格な TypeScript では失敗します。また、値全体のアサーションのほうがミューテーションに対しても堅牢です。
動作を通してコードに到達する
検証コードには、それが保護する動作をテストして到達してください。
// Tests covering validation WITHOUT testing validator directly
describe('processPayment', () => {
it('should reject negative amounts', () => {
const payment = getMockPayment({ amountMinorUnits: -100, currency: 'GBP' });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
it('should reject amounts over 10000', () => {
const payment = getMockPayment({ amountMinorUnits: 1_500_000, currency: 'GBP' });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
it('should reject invalid CVV', () => {
const payment = getMockPayment({ cvv: '12' });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
it('should process valid payments', () => {
const payment = getMockPayment({ amountMinorUnits: 10_000, currency: 'GBP', cvv: '123' });
const result = processPayment(payment);
expect(result.success).toBe(true);
});
});
// ✅ Result: validation branches are exercised through the public behavior
重要なポイント: カバレッジが下がったときは、「何の行を見落としているか?」ではなく、**「どのビジネス動作をテストしていないか?」**を考えてください。
テストに合わせるだけの抽出はしない
対応する単体テストを作るためだけに、関数を独立したファイルに抽出しないでください。まとまりのある契約や境界が生まれる場合、可読性が向上する場合、共有された知識を表す場合(refactoring スキルを参照)、または責務を分離する場合に抽出してください。動作のテストが難しいことで実際の依存関係の境界が明らかになった場合は、実装の詳細をテストして問題を隠すのではなく、finding-seams を使ってください。
インラインのコードは、多くの場合、利用側の動作テストを通じてテストできます。それによって失敗の範囲が広くなりすぎたり、依存関係を制御できなくなったりする場合は、その難しさを設計上の証拠として捉え、テストファイルをまねるためだけにヘルパーを作るのではなく、まとまりのある境界を設けてください。
アンチパターンは、抽出したヘルパーとテストファイルを 1 対 1 で対応させることです(下記「1 対 1 の対応を避ける」を参照)。抽出したヘルパーは利用側の実装詳細です。利用側の動作をテストしてください。
❌ 誤り — 単一の箇所からしか使われないヘルパーを抽出し、専用のテストファイルを作る:
// prepare-participant-data.ts (new file, one caller)
export const prepareParticipantData = (items: Item[]) => ({
yourClaims: items.filter(i => i.isClaimed && i.isClaimedByCurrentUser),
available: items.filter(i => !i.isClaimedByCurrentUser),
});
// prepare-participant-data.test.ts (tests the helper directly)
it('filters claims', () => { ... });
✅ 正しい — 利用側の関数にインラインで記述し、その動作を通じてテストする:
// load-participant-view.ts
export const loadParticipantView = async (db: Db, eventId: EventId, userId: UserId) => {
const items = await getItems(db, eventId, userId);
const yourClaims = items.filter(i => i.isClaimed && i.isClaimedByCurrentUser);
const available = items.filter(i => !i.isClaimedByCurrentUser);
return { yourClaims, available };
};
// The behavioral test for loadParticipantView covers the filtering:
it('returns claimed gifts in yourClaims and unclaimed in available', async () => {
const result = await loadParticipantView(db, eventId, userId);
expect(result.yourClaims).toHaveLength(1);
expect(result.available).toHaveLength(2);
});
抽出が妥当な場合: フィルタリングにまとまりのある再利用可能な意味がある場合や、複数の利用側が依存する場合は、抽出してください。動作を正当に担う最も狭い公開境界でテストしてください。統合のために、利用側のテストが引き続き必要になる場合もあります。
テストファクトリーパターン
テストデータが繰り返し使われる場合、ネストされている場合、または名前付きフィクスチャビルダーを使うほうが明確な場合は、オーバーライドを任意に指定できるファクトリー関数を使用してください。本当に一度しか使わないスカラー値はインラインにしてください。判断基準は好みではなく、機械的なものです。同じファイル内の複数のテストで同じオブジェクト形状を構築する場合は、その形状を it.each の行に展開する場合や、別のオブジェクトから展開してフィールドを 1 つ変更する場合も含め、その形状用のファクトリーを作成し、異なるフィールドをオーバーライドにします。
基本原則
- シナリオに合った有効なオブジェクトを返し、意図的に無効にする場合はそのことを明示します
- 言語とモデルに合う場合は、型付きオーバーライドを使用します
- ファクトリー本体を書く前に、テスト対象のモジュールがその型のために何をエクスポートしているか確認します。オブジェクト用のスキーマをエクスポートしている場合は、そのスキーマの parse を呼び出してオブジェクトを構築します。型注釈付きリテラルを返すファクトリーは、スキーマを再定義する場合と同じく、本番環境の契約を見落とします。型注釈はコンパイラーによって型と照合されますが、コードが実際に検証する境界に対してフィクスチャをチェックするものはありません。スキーマがない場合に限り、通常のリテラルを返してください。parse するためだけにスキーマを作ることは避けてください
- テストごとに新しい状態を使うことを優先します。各テストでファクトリー呼び出しから独自のデータを構築してください。セットアップが分離され、クリーンアップが確実であればライフサイクルフックを使ってもかまいませんが、共有の
letにバインドしたデータをテストごとに再構築する方法は避けてください。これは、下記のアンチパターンにフックを着せただけのものです。データを共有のletや、データを再構築するbeforeEachに保持している既存のファイルにテストを追加する場合は、そのセットアップをファクトリーに置き換える変更も同時に行ってください。既存のデータを展開する新しいテストを書いてはいけません。テストファイルに手を入れたら、追加した行だけでなく、そのファイル全体の状態管理に責任を持ってください
基本パターン
const getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: '[email protected]',
role: 'user',
...overrides,
});
};
// Usage
it('creates user with custom email', () => {
const user = getMockUser({ email: '[email protected]' });
const result = createUser(user);
expect(result.success).toBe(true);
});
ファクトリーの完全な例
import { UserSchema } from '@/schemas'; // Import real schema
const getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: '[email protected]',
role: 'user',
isActive: true,
createdAt: new Date('2024-01-01'),
...overrides,
});
};
既存のスキーマを再利用する理由: コントラクトに沿ったフィクスチャを本番環境との境界に合わせて保ち、コントラクトを再定義せずにスキーマの変更を検出できます。純粋な内部値や、意図的に無効にしたフィクスチャでは、スキーマの解析は不要な場合があります。
ヒント: 関連するフィールドが一部だけのファクトリーでは、オーバーライド用のパラメーターに Partial<Pick<T, 'field1' | 'field2'>> を使い、呼び出し側がカスタマイズできる項目を制限します(単独の Pick では選択したすべてのフィールドが必須になるため、1つだけをオーバーライドしようとするとコンパイルできません)。
ファクトリーの合成
ネストしたオブジェクトには、ファクトリーを合成します。
const getMockItem = (overrides?: Partial<Item>): Item => {
return ItemSchema.parse({
id: 'item-1',
name: 'Test Item',
weightGrams: 100,
...overrides,
});
};
const getMockOrder = (overrides?: Partial<Order>): Order => {
return OrderSchema.parse({
id: 'order-1',
items: [getMockItem()], // ✅ Compose factories
customer: getMockCustomer(), // ✅ Compose factories
payment: getMockPayment(), // ✅ Compose factories
...overrides,
});
};
// Usage - override nested objects
it('calculates total with multiple items', () => {
const order = getMockOrder({
items: [
getMockItem({ weightGrams: 100 }),
getMockItem({ weightGrams: 200 }),
],
});
expect(calculateTotalWeightGrams(order)).toBe(300);
});
アンチパターン
❌ 誤り:テストスイート全体で1つの変更可能なオブジェクトを共有する
const user: User = { id: 'user-123', name: 'Test User', ... };
it('test 1', () => {
user.name = 'Modified User';
});
it('test 2', () => {
expect(user.name).toBe('Test User'); // Order-dependent failure
});
✅ 正しい:テストごとに新しい状態を用意する
it('test 1', () => {
const user = getMockUser({ name: 'Modified User' }); // Fresh state
// ...
});
it('test 2', () => {
const user = getMockUser(); // Fresh state, not affected by test 1
expect(user.name).toBe('Test User'); // ✅ Passes
});
❌ 誤り:不完全なオブジェクト
const getMockUser = () => ({
id: 'user-123', // Missing name, email, role!
});
✅ 正しい:完全なオブジェクト
const getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: '[email protected]',
role: 'user',
...overrides, // All required fields present
});
};
❌ 誤り:テスト内でスキーマを再定義する
// ❌ Schema already defined in src/schemas/user.ts!
const UserSchema = z.object({ ... });
const getMockUser = () => UserSchema.parse({ ... });
✅ 正しい:実際のスキーマをインポートする
import { UserSchema } from '@/schemas/user';
const getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: '[email protected]',
...overrides,
});
};
カバレッジだけを装うテストの検出
コードを実行しても、動作を検証できていないパターンに注意してください。
パターン 1:テスト対象の関数をモックする
❌ 誤り - カバレッジは 100% になりますが、何もテストしていません。
it('calls validator', () => {
const spy = vi.spyOn(validator, 'validate');
validator.validate(payment);
expect(spy).toHaveBeenCalled(); // Meaningless assertion
});
✅ 正しい - 実際の動作をテストします。
it('should reject invalid payment', () => {
const payment = getMockPayment({ amountMinorUnits: -100, currency: 'GBP' });
const result = validate(payment);
expect(result).toEqual({ success: false, error: expect.stringContaining('Amount must be positive') });
});
パターン 2:関数が呼び出されたことだけをテストする
❌ 誤り - 動作を検証していません。
it('processes payment', () => {
const spy = vi.spyOn(processor, 'process');
handlePayment(payment);
expect(spy).toHaveBeenCalledWith(payment); // So what?
});
✅ 正しい - 結果を検証します。
it('should process payment and return transaction ID', () => {
const payment = getMockPayment();
const result = handlePayment(payment);
expect(result).toEqual({ success: true, data: { transactionId: expect.any(String) } });
});
例外: 公開 API 経由で渡されたコールバックを検証することは、カバレッジだけを装うテストではなく、動作のテストです。コンポーネントや関数がコールバック(例:onSubmit プロパティ)を受け取る場合、そのコールバックの契約自体が出力です。expect(handleSubmit).toHaveBeenCalledWith(...) は、観測可能な動作を検証します。このパターンで禁止されるのは、呼び出し側が渡していない内部の協調要素をスパイすることです。
パターン 3:単純なゲッター/セッターをテストする
❌ 誤り - 動作ではなく、実装をテストしています。
it('sets retry limit', () => {
worker.setRetryLimit(3);
expect(worker.getRetryLimit()).toBe(3); // Trivial
});
✅ 正しい - 意味のある動作をテストします。
it('should calculate shipment weight', () => {
const order = createOrder({ items: [item1, item2] });
expect(order.calculateWeightGrams()).toBe(230);
});
パターン 4:行カバレッジ 100%、分岐カバレッジ 0%
❌ 誤り - エッジケースが不足しています。
it('validates payment', () => {
const result = validate(getMockPayment());
expect(result.success).toBe(true); // Only happy path!
});
// Missing: negative amounts, invalid CVV, missing fields, etc.
✅ 正しい - すべての分岐をテストします。
describe('validate payment', () => {
it('should reject negative amounts', () => {
const payment = getMockPayment({ amountMinorUnits: -100, currency: 'GBP' });
expect(validate(payment).success).toBe(false);
});
it('should reject amounts over limit', () => {
const payment = getMockPayment({ amountMinorUnits: 1_500_000, currency: 'GBP' });
expect(validate(payment).success).toBe(false);
});
it('should reject invalid CVV', () => {
const payment = getMockPayment({ cvv: '12' });
expect(validate(payment).success).toBe(false);
});
it('should accept valid payments', () => {
const payment = getMockPayment();
expect(validate(payment).success).toBe(true);
});
});
テストと実装を自動的に 1 対 1 で対応させない
実装ファイルごとに反射的にテストを作成しないでください。安定した動作やコントラクトを軸にテストを構成します。ただし、そのファイル自体がテスト対象となる公開単位であれば、1 対 1 の対応でも問題ありません。
❌ 誤り:
src/
payment-validator.ts
payment-processor.ts
payment-formatter.ts
tests/
payment-validator.test.ts ← 1:1 mapping
payment-processor.test.ts ← 1:1 mapping
payment-formatter.test.ts ← 1:1 mapping
✅ 正しい:
src/
payment-validator.ts
payment-processor.ts
payment-formatter.ts
tests/
process-payment.test.ts ← Tests behavior, not implementation files
理由: テストを変更せずに、実装の詳細をリファクタリングできます。テストでは、内部のコード構成にかかわらず、動作が正しいままであることを検証します。
要約チェックリスト
テストを作成するときは、以下を確認してください。
- 主張で指定したレイヤーにおいて、対象のパブリックインターフェースを通じて動作をテストしていること(実装の詳細をテストしたり、代わりに下位レイヤーを使ったりしていないこと)
- テスト対象の関数をモックしていないこと
- プライベートメソッドや内部状態をテストしていないこと
- ファクトリー関数がシナリオに適したオブジェクトを返し、意図的に無効なものにはその旨を明示していること
- 適切な場合は、テストで再定義せず既存の本番用スキーマを再利用していること
- オーバーライドが、言語とモデルに対して型安全であること
- テスト状態が分離され、ライフサイクルのセットアップで確実にクリーンアップされること
- テスト対象コードにある各しきい値について、しきい値を大きく超えたケースだけでなく、しきい値ちょうどのケースもテストしていること
- 正規化や変換を行う呼び出し(大文字・小文字の統一、トリミング、ソート、丸め、デフォルト値の設定)ごとに、その呼び出しを削除した場合に答えが変わるテストがあること
- エッジケースをカバーしていること(正常系だけでないこと)
- 実装をリファクタリングしてもテストが通ること
- デフォルトでは、実装の形ではなく、安定した動作や契約に基づいてテストを構成していること
- TDD の内側のループでは、スイート全体を繰り返し実行するのではなく、対象を絞ったウォッチや関連・影響範囲のセレクターを使うこと
- GREEN/REFACTOR では、ランナー、ワークスペースオーケストレーター、またはリポジトリのマッピングから導き出した、影響を受ける範囲全体を実行すること。信頼できる依存関係グラフがない場合は、文書化された、担当スイートと既知の利用箇所を組み合わせる代替手順を使い、不確実な場合は範囲を広げ、手作業で選んだテストファイルだけを実行しないこと
- ウォッチャーを使用する場合は新しいテストもその対象に加え、使用しない場合はテスト作成後に影響範囲を対象とした単発実行を再度行うこと。実行したテストが少なくとも1つの想定テストを実行していることを、
--passWithNoTestsを使わずに、すべての報告結果で確認すること - ウォッチャーとその子プロセスを停止していること。ウォッチモードではないスイート全体の実行が、最終ツリーに対して最新であること。また、ミューテーションテストまたは別の証拠が、対象リポジトリのポリシーを満たしていること