testing-best-practices について
ユニット、統合、コンポーネント、マイクロサービス、API のテストを作成・レビューするためのガイドラインです。テストの簡潔さと独立性を重視し、データ、アサーション、モック、DOM の具体的なルールを示します。
主な機能
- 01テストの長さと複雑さを制限
- 02Arrange・Act・Assert の各段階を要求
- 03自己完結したテストデータを推奨
- 04アサーションとモックの方法を規定
- 05DOM とデータベースのテストを網羅
活用シーン
- 01テストをレビューし違反ルール番号を特定
- 02新しいテストの簡潔な構成を設計
- 03テストデータとモックのチーム規約を統一
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ royrvd/ golden-testing-rules-for-ai --skill testing-best-practicesインストール後、Claude Code で/testing-best-practicesを入力して読み込まれたか確認します。
同梱ファイルreferences/e2e-testing-rules.md
詳細
AI 向けテストルール
テストをシンプルで、きれいで、一貫性があり、短く保つことを重視するテストの専門家として行動してください。以下に、従うべきベストプラクティスをまとめます。テストに問題を見つけた場合は、違反している箇条書きの番号を示してください。
これらのルールは、複数のプロセスやコンポーネントにまたがるエンドツーエンドテストには適用されません。単体テスト、統合テスト、コンポーネントテスト、マイクロサービステスト、API テストにのみ適用されます。バックエンドをモックしていないテストに気づいた場合、それはエンドツーエンドテストです。その場合は、references/e2e-testing-rules.md のルールを適用してください。
最も重要な 6 つのルール!
テストは決して、保守が必要な別のシステムになってはいけません。そのため、複雑さを極端に低く保ちます。読みやすさを非常にシンプルにすることを最優先します。これらのルールに従えない場合は、テストのコードを書くのを必ず止めてください。このドキュメントのすべてのルールは必須ですが、以下の 6 つは特に重要です。
- 重要:テストのステートメントは 10 個以下にしてください。#customize
- 重要:優れた物語のように、テストには不要な詳細を含めず、テスト結果に直接影響する詳細をすべて含めてください。
- 重要:フラットなステートメント以外は認められません。if/else、ループ、try-catch、console.log は使用しないでください。
- 重要:テストの範囲に応じて、テスト対象コードのすべてのレイヤー(例:フロントエンドのページ、バックエンドのマイクロサービス)をテストしてください。つまり、外部システムへの呼び出しを行う部分以外、アプリケーション内部の要素は決してモックしないでください。
- 🔫 決定的証拠の原則:重要:アサーション/期待値の段階で使うデータや前提は、結果と原因が読み手に明確に伝わるよう、すべて最初に Arrange フェーズに記述してください。
- 重要:各テストは自己完結させ、他のテストの状態や生成された成果物に依存させないでください。したがって、テストが何らかの状態に依存する場合は、その状態をテスト内で作成するか、フックで作成されていることを確認してください。
セクション A - テストの構造
A. 1. テストタイトルは「When {case/scenario}, then {some expectation}」のパターンにしてください。例:「When adding a valid order, then it should be retrievable」#customize
A. 3. ステートメントと式は 10 個以下にしてください。複数行に分割された単一の式は数えません。#customize
A. 4. Arrange フェーズのデータを Assert フェーズで使う場合、値を重複して記述しないでください。代わりに、Arrange で用意したデータを直接参照してください。これにより、Arrange フェーズにある 🔫 決定的証拠が結果につながることを読み手に示せます。例:expect(result.id).toBe(activeOrder.id) を使用し、expect(result.id).toBe('123') は使用しないでください。
A. 5. テストには少なくとも 3 つのフェーズが必要です:Arrange、Act、Assert。テスト内に各フェーズ名を記述するか、第 2 および第 3 フェーズの前に改行を入れてください。
A. 10. アサーションは 3 個以下にしてください。
A. 13. 完全にフラットな構造にしてください。try-catch、ループ、コメント、console.log は使用しないでください。
A. 15. 🥨 パンくずの原則:重要:テストに直接影響するものはすべて、テスト内に直接記述してください(例:Assert フェーズで確認するデータ)。テストに暗黙的な影響を及ぼす可能性があるものは、ローカルのテストフックに記述してください(例:外部セットアップではなく、beforeEach で認証をモックする)。余計なセットアップファイルによる隠れた影響は避けてください。
A.18. 快適にテストできるよう、すべての変数に暗黙的または明示的な型を付けてください。'any' 型は使用しないでください。意図的に不正な入力を作成する必要がある場合は、'myIllegalObject as unknown as LegalType' を使用してください。
A.23. 明確さを保つため、アサーションはテスト内のみに記述し、ヘルパーやフック内には記述しないでください。
A.25. アサーションは /Assert フェーズ内のみに記述し、テストの開始部分や途中には記述しないでください。
A.28. 特定のセットアップに 3 行以上必要な場合は、/test/helpers フォルダー内の関数に移してください。全体の Arrange が 2 行を超えてもかまいませんが、1 つの目的を達成するための個別のセットアップが 3 行以上になる場合は、ヘルパーファイルに切り出してください。
セクション B - テストロジック
B. 3. 🔫 決定的証拠の原則:重要:結果と原因を読み手に明確に伝えるため、アサーションフェーズで使う各データや前提は、必ず最初にアレンジフェーズに記述してください
B. 5. テスト結果の理解に直接関係しない詳細は、テストに含めないでください
B. 10. 冗長なアサーションは避けてください
B. 15. 膨大なデータセットをアサーションして比較するのではなく、テストでは特定のトピックや領域に焦点を当ててください
B. 20. テストで何らかのレコードやデータが存在することを前提とする場合、アレンジフェーズで事前に作成してください
B. 23. 実装の詳細をテストしないでください。画面要素のようなユーザー向けの動作ではなく、内部実装を確認するアサーションがある場合にのみ、この問題を指摘してください
B. 25. setTimeout や page.waitForTimeout(2000) など、時間に基づく待機は避けてください
B. 28. 各テストの前(beforeEach)に、テスト間に漏れ出す可能性があるもの(モック、環境変数、ローカルストレージ、グローバル変数、その他のテスト同士に影響を与えるリソース)をクリーンアップしてください
セクション C - テストデータ
C.3. JSON やエンティティなどのデータは、data フォルダー内のデータファクトリーから取得してください。各データ型には専用のデータファクトリーファイルを用意し、エンティティを構築するメイン関数(例:buildOrder、buildUser)を含めてください
C.4. ファクトリー関数はデフォルトデータを返すと同時に、呼び出し側から特定のフィールドを上書きできるようにしてください。これにより、各テストで特定のフィールド値を変更できます
C.5. 日付、住所など、ドメイン固有ではないフィールドに共通の汎用データを設定する場合は、fakerjs など、現実的な実世界のデータを提供するライブラリを使用してください
C.7. データファクトリー関数の入力パラメーターと出力パラメーターには、テスト対象のコードで使われているものと同じ型を指定してください
C.10. テストデータには、ダミー値ではなく、意味のあるドメインデータを使用してください
C.15. 複数の選択肢があるフィールドを構築する場合、すべての選択肢をテストできるよう、デフォルトでは選択肢をランダムにしてください
C.20. リスト/配列のデフォルトの項目数は 2 つにしてください。なぜでしょうか? 0 件や 1 件はバグを見つけるうえで単純すぎます。一方で 20 件では多すぎます。2 件なら、シンプルさと現実性のバランスが取れています
これらのルールに従った、優れたデータファクトリーの例:
import { faker } from "@faker-js/faker";
import { FileContext } from "../types";
export function buildFileFromIDE(overrides: Partial<FileContext> = {}): FileContext {
return {
path: faker.system.filePath(),
type: faker.helpers.arrayElement(["file", "folder"]),
...overrides,
};
}
セクション D - アサーション
D.7. 独自のコード、ループ、Array.prototype 関数は避け、配列も含めて、組み込みの expect API を使用してください
D.11. 失敗を検出するために必要最小限のアサーションを使用し、冗長なチェックは避けてください。次のようにしてください:expect(response).toEqual([{id: '123'}, {id: '456'}]。次のようにするのではなく:
expect(response).not.toBeNull() // redundant
expect(Array.isArray(response)).toBe(true) // redundant
expect(response.length).toBe(2) // redundant
expect(response[0].id).toBe('123') // redundant
1 つのアサーションで、null、配列以外の値、誤ったデータを検出できます
D.13. 失敗時に比較結果の詳細がすべて表示されるアサーションマッチャーを優先してください。完全な差分が表示される expect(actualArray).toEqual(expectedArray) を使用し、true/false しか表示されない expect(actualArray.contains(expectedValue)).toBeTrue() は使用しないでください
D.15. 3 つを超えるフィールドを持つオブジェクトをアサーションする場合、データファクトリーから期待値オブジェクトを取得し、最も重要な 3 つの値を上書きしてください。アサーション対象となる重要な値が 3 つを超える場合は、テストケースをもう 1 つに分けてください
Section E - モック
E.1. 重要:テストの対象範囲外にある外部の連携先(例:メールサービスのクライアント、決済ゲートウェイ)を呼び出すコードのみをモックしてください。例外:ほかの方法では発生させられない重要なイベントをシミュレートするために必要なモック
E.3. モック対象のコードの型/インターフェースを必ず使用してください。そうすれば、実装が変更されたときにモックのコンパイルが失敗し、新しい契約に合わせた更新が必要になります
E.5. モックはテストファイル内で直接定義してください。結果に直接影響する場合はテストの Arrange フェーズで、コンテキストに必要な場合は beforeEach で定義します。挙動が不可解に変わるような外部のセットアップファイルにモックを隠さないでください
E.7. クリーンな状態を保つため、beforeEach ですべてのモックをリセットしてください
E.9. 既知の URL に HTTP リクエストを送信するコードをモックする場合は、関数のモックよりもネットワークインターセプト(MSW、Nock)を優先してください。これにより、より多くのコードをテスト対象に含められます
Section F - DOM を使ったテスト
React-testing-library、Playwright、StoryBook などのフレームワークに適しています
F.1. 重要:ARIA ロール、ラベル、またはアクセシブルネームに基づく、ユーザー向けロケーターのみを使用してください(例:getByRole、getByLabel)。test-id(例:.getByTestId)、CSS セレクター、または ARIA に基づかないロケーターは使用しないでください
F.3. ページの構造やレイアウトを想定したり、依存したりしないでください。nth(i)、first()、last() などの位置指定セレクターは使用しないでください
F.5. 要素を安全にアサートするには、フレームワークの仕組みを使用してください。フレームワークが再レンダリングの終了を確実に判定できる場合(例:testing-library)は、通常の await 不要なアサーションを使用してください。レンダラーと直接やり取りしない Playwright などのフレームワークでは、自動再試行されるアサーション(「web-first assertions」とも呼ばれます)を await 付きで使用してください:await expect(locator).toContainText('some string');
F.9. 内部要素の表示を待つ方法(例:Playwright waitForSelector)は避けてください。テストが実装に依存してしまいます。自動再試行されるアサーションが確実に待機します
F.14. 外部システムへのアクセスやアサーションは避けてください。代わりに、必要に応じてスタブ化したレスポンスをシミュレートし、ナビゲーションが行われたことをアサートしてください
Section G - データベースを使ったテスト
G.3. 複数のレコードを追加して、意図したものだけが変更されたことをアサートし、望ましくない副作用をテストしてください。例:await api.delete('/order/123') の後、await api.get('/order/456') が引き続き 200 を返すことを確認します
G.5. 自動生成されるフィールドのレスポンススキーマは、型マッチャーを使ってテストしてください。例:expect(response).toMatchObject({ id: expect.any(Number), createdAt: expect.any(String) })
G.7. 一意である必要があるフィールドには、意味のあるドメインデータと一意のサフィックスの両方を含めてランダム性を加えてください。例:メールアドレスが一意である場合:{ email: ${faker.internet.email()}-${faker.string.nanoid(5)} }
G.9. 内部実装への依存を避けるため、新しいデータの状態は DB への直接クエリではなく、公開 API を使ってアサートしてください。例:await api.post('/order', newOrder) の後、await api.get('/order/${id}') で確認します
G.12. テストのメタデータ(国、通貨)とコンテキストデータ(テストユーザー)のみ、テストの外で事前にシードしてください。テスト固有のレコードは各テスト内で作成します。例:グローバルシードに国の一覧を含め、テスト内で独自の注文を作成します
G.14. 重要:各テストは自身のレコードのみを操作してください。テスト間でテストデータを共有しないでください
G.18. 望ましくない連鎖的な削除や更新をテストしてください。例:親レコードを削除し、子レコードが適切に処理されること(ビジネスルールに従って保持されるか、適切に削除されるか)をアサートします
Section I - テスト対象
I.7. 🚀 もう一歩踏み込む原則:シナリオをテストする際は、少しだけ範囲を広げてテストすることを目指してください。アイテムの保存をテストするなら、1つではなく2つ使います。グリッドのフィルタリングをテストするなら、表示されるべきでないアイテムも確認します
I.10. 🔥 意図的に火種を選ぶ原則:各設定やデータでは、失敗につながる可能性がより高い選択肢を選んでください。たとえば、ユーザーロールを選ぶ場合は、最小権限のロールを選びます
例
悪いテストの例
// BAD TEST EXAMPLE - Violates multiple best practices
it('should test orders report filtering functionality', async () => { // 👎🏻 violates A.1
const adminUser = { role: 'admin' } // 👎🏻 violates I.10
// Setting up mocks for internal implementation details
const mockOrderService = vi.fn() // 👎🏻 violates E.1
const mockFilterService = vi.fn() // 👎🏻 violates E.1
// Dummy meaningless data
const testData = [ // 👎🏻 violates C.8
{ id: 1, name: 'test1', status: 'foo', date: '2023-01-01' },
{ id: 2, name: 'test2', status: 'bar', date: '2023-01-02' }
]
// No clear arrange phase - mixing setup with assertions
render(<OrdersReport data={testData} onFilter={mockFilterService} />)
// Getting internal component state instead of user-facing behavior
const component = screen.getByTestId('orders-report') // 👎🏻 violates F.1
const internalState = component.querySelector('.internal-filter-state') // 👎🏻 violates F.1
try { // 👎🏻 violates A.13
const filterButton = screen.getByRole('button', { name: 'Filter Active' })
await userEvent.click(filterButton)
// Custom assertion logic instead of built-in expect
let foundItems = [] // 👎🏻 violates D.7
const rows = screen.getAllByRole('row')
for (const row of rows) { // 👎🏻 violates A.13, D.7
if (row.textContent?.includes('Active')) {
foundItems.push(row)
}
}
// Asserting data that was never arranged in the test
expect(foundItems.length).toBe(5) // 👎🏻 violates B.3, B.20
// Testing implementation details
expect(mockOrderService).toHaveBeenCalled() // 👎🏻 violates B.23
expect(internalState).toHaveClass('filtered-state') // 👎🏻 violates B.23
// Too many assertions
expect(component).toBeInTheDocument() // 👎🏻 violates A.10
expect(screen.getByText('Active Orders')).toBeVisible() // 👎🏻 violates A.10
expect(filterButton).toHaveAttribute('aria-pressed', 'true') // 👎🏻 violates A.10
expect(rows).toBeDefined() // 👎🏻 violates B.10, D.11
expect(rows).not.toBeNull() // 👎🏻 violates B.10, D.11
expect(rows.length).toBeGreaterThan(0) // 👎🏻 violates B.10, D.11
} catch (error) { // 👎🏻 violates A.13
console.log('Filter test failed:', error) // 👎🏻 violates A.13
throw new Error('Test setup failed')
}
// More irrelevant details not related to filtering
const headerElement = screen.getByRole('banner')
expect(headerElement).toHaveTextContent('Dashboard') // 👎🏻 violates B.5
}) // 👎🏻 violates A.3 (too many statements), A.5 (no clear AAA phases)
良いテストの例
beforeEach(() => {
const currentUser = buildUser({ name: faker.person.fullName(), role: 'viewer' }) // 🔥 The deliberate fire principle
http.get('/api/user/1', () => HttpResponse.json(currentUser)) // 🥨 The breadcrumb principle
})
test('When filtering by active status, then only active orders are displayed', async () => {
// Arrange
const activeOrder = buildOrder({ customerName: faker.person.fullName(), status: 'active' })
const completedOrder = buildOrder({ customerName: faker.person.fullName(), status: 'non-active' }) // 🔫 The smoking gun principle
http.get('/api/orders', () => HttpResponse.json([activeOrder, completedOrder]))
const screen = render(<OrdersReport />)
// Act
await userEvent.click(screen.getByRole('button', { name: 'Filter by Active' }))
// Assert
expect.element(screen.getByRole('cell', { name: activeOrder.customerName })).toBeVisible()
expect.element(screen.getByRole('cell', { name: completedOrder.customerName })).not.toBeVisible() // 🚀 The extra mile principle
})
最後に
すべてのルールを守るよう努めてください。「特に重要な6つのルール」はさらに重要です。2回読んでください。