test-principles

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test Principles - テスト設計の原則

Test Principles - Test Design Principles

Google の "Software Engineering at Google" に基づく 6 つのテスト設計原則を適用し、保守性が高く意味のあるテストを書くためのガイド。
テストは「動くことの証明」ではなく「将来の変更に対する安全網」。壊れやすいテストは安全網にならず、むしろ開発速度を落とす。この原則群は、テストを長期的に価値あるものにするための指針である。
A guide to writing maintainable and meaningful tests by applying 6 test design principles based on Google's "Software Engineering at Google".
Tests are not "proof that it works" but a "safety net for future changes". Fragile tests do not serve as a safety net; instead, they slow down development. These principles are guidelines to make tests valuable in the long term.

コア原則

Core Principles

1. 振る舞いをテストせよ、メソッドをテストするな

1. Test behavior, not methods

テストの構造はメソッド名ではなく「ユーザーから見た振る舞い」で決める。メソッド名ベースでテストを分けると、リファクタリングのたびにテストも書き直す羽目になる。振る舞いベースなら、内部実装が変わっても振る舞いが同じならテストは壊れない。
  • 1 つのメソッドに複数の振る舞いがあれば、振る舞いごとに別のテストを書く
  • 1 つの振る舞いが複数のメソッドにまたがっていれば、1 つのテストでカバーする
  • テストケース名は振る舞いを日本語で記述する
go
// NG: メソッド名ベース
func TestCalculatePrice(t *testing.T) { ... }

// OK: 振る舞いベース
func TestCalculatePrice_割引が適用される場合は割引後の価格を返す(t *testing.T) { ... }
func TestCalculatePrice_在庫がない場合はエラーを返す(t *testing.T) { ... }
The structure of tests should be determined by "user-visible behavior" rather than method names. If tests are divided based on method names, you will have to rewrite tests every time you refactor. With behavior-based tests, tests won't break as long as the behavior remains the same even if the internal implementation changes.
  • If a single method has multiple behaviors, write separate tests for each behavior
  • If a single behavior spans multiple methods, cover it with one test
  • Name test cases in Japanese to describe the behavior
go
// NG: Method-name based
func TestCalculatePrice(t *testing.T) { ... }

// OK: Behavior-based
func TestCalculatePrice_ReturnsDiscountedPriceWhenDiscountIsApplied(t *testing.T) { ... }
func TestCalculatePrice_ReturnsErrorWhenStockIsOutOfStock(t *testing.T) { ... }

2. DRY よりも DAMP を優先せよ

2. Prefer DAMP over DRY

DAMP = Descriptive And Meaningful Phrases。テストコードでは再利用性よりも可読性を優先する。各テストケースは、そのテストだけ読めば何をテストしているか完全に理解できるべき。
テストが失敗したとき、開発者はそのテストケースだけを読んで問題を理解したい。ヘルパー関数を追いかけて 3 ファイル読まないと理解できないテストは、デバッグ効率を著しく下げる。
  • テストデータはテストケース内にインラインで記述する
  • ヘルパー関数は使ってよいが、テストの意図を隠す過度な抽象化は避ける
  • table-driven test の各ケースには、検証に必要な情報をすべて含める
go
// NG: 重要な値がヘルパーの奥に隠れている
order := createDefaultOrder()

// OK: テストに必要な値が見える
order := &Order{
    ID:        "order-001",
    Status:    StatusPending,
    CreatedAt: time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC),
}
DAMP = Descriptive And Meaningful Phrases. Prioritize readability over reusability in test code. Each test case should be completely understandable just by reading that test alone.
When a test fails, developers want to understand the problem by reading only that test case. Tests that require reading 3 files to trace helper functions significantly reduce debugging efficiency.
  • Write test data inline within the test case
  • You can use helper functions, but avoid excessive abstraction that hides the test's intent
  • Each case in a table-driven test should include all information necessary for verification
go
// NG: Important values are hidden deep in a helper
order := createDefaultOrder()

// OK: Values needed for the test are visible
order := &Order{
    ID:        "order-001",
    Status:    StatusPending,
    CreatedAt: time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC),
}

3. テストにロジックを入れるな

3. No logic in tests

テストコードに
if
for
switch
を入れない。テストで値を動的に計算しない。期待値はリテラルで書く。
テストにロジックがあると、テストが失敗したときに「実装のバグ」なのか「テストのバグ」なのか区別がつかない。テストは「与えて → 実行して → 検証する」の直線的な流れにすることで、テスト自体の信頼性を担保する。
go
// NG: 期待値を計算で算出
want := basePrice * (1 - discountRate)

// OK: 期待値をリテラルで書く
want := 800 // 1000円の20%割引
Do not include
if
,
for
, or
switch
in test code. Do not dynamically calculate values in tests. Write expected values as literals.
If tests contain logic, when a test fails, it's impossible to distinguish whether it's an "implementation bug" or a "test bug". Tests should follow a linear flow of "given → execute → verify" to ensure the reliability of the tests themselves.
go
// NG: Calculating expected value
want := basePrice * (1 - discountRate)

// OK: Writing expected value as a literal
want := 800 // 20% discount on 1000 yen

4. インタラクションではなく状態をテストせよ

4. Test state, not interactions

モックの呼び出し回数や引数順序ではなく、最終的な状態や結果を検証する。インタラクションテストは実装の内部詳細に結合するため、リファクタリングでテストが壊れる主因となる。
  • 「何が起こったか」ではなく「結果どうなったか」を検証する
  • モックは外部依存(API、DB)の置き換えに限定する
  • 可能であれば実際のデータストアを使ったテストを優先する
go
// NG: 呼び出し回数に依存
if mockRepo.callCount != 3 { t.Error(...) }

// OK: 最終結果を検証
if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("mismatch (-want +got):\n%s", diff)
}
Verify the final state or result rather than the number of mock calls or argument order. Interaction tests are coupled to internal implementation details, making them the main cause of broken tests during refactoring.
  • Verify "what the result was" rather than "what happened"
  • Limit mocks to replacing external dependencies (APIs, DBs)
  • Prefer tests using actual data stores when possible
go
// NG: Dependent on call count
if mockRepo.callCount != 3 { t.Error(...) }

// OK: Verify final result
if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("mismatch (-want +got):\n%s", diff)
}

5. 公開 API を通じてテストせよ

5. Test through public APIs

モジュールの公開インターフェースを通じてテストする。プライベートメソッドや内部実装の詳細を直接テストしない。
プライベートメソッドは実装の詳細であり、リファクタリングで自由に変更できるべき。公開 API を通じてテストすれば、内部構造を変更してもテストは壊れず、かつ公開 API のコントラクトが守られていることを保証できる。
  • エクスポートされた関数・メソッドを通じてテストする
  • フレームワークやライブラリの動作はテストしない
  • テスト用に内部状態を export しない
Test through the public interface of a module. Do not directly test private methods or internal implementation details.
Private methods are implementation details that should be freely modifiable during refactoring. Testing through public APIs ensures that tests won't break even if the internal structure changes, and guarantees that the public API contract is upheld.
  • Test through exported functions and methods
  • Do not test the behavior of frameworks or libraries
  • Do not export internal state for testing purposes

6. 完全かつ簡潔にせよ

6. Be complete and concise

各テストは 1 つの振る舞いのみを検証する。テストに必要な情報はすべてテスト内に含め、検証に無関係なセットアップは最小限にする。
  • テスト名は検証する振る舞いを正確に記述する
  • 他のテストを読まないと理解できないテストにしない
  • 不要なフィールドのセットアップは省略する
Each test should verify only one behavior. Include all information necessary for the test within the test, and minimize setup unrelated to verification.
  • Accurately describe the behavior being verified in the test name
  • Do not create tests that require reading other tests to understand
  • Omit setup of unnecessary fields

サブコマンド

Subcommands

review - テストコードレビュー

review - Test Code Review

$ARGUMENTS
からレビュー対象のテストファイルを特定し、6 原則に基づいてレビューする。
手順:
  1. 対象テストファイルを
    Glob
    で検索し
    Read
    で読み込む
  2. テスト対象の実装コードも読み込む(公開 API テストの確認のため)
  3. 各原則に照らして問題点を検出する
  4. レビュー結果を以下のフォーマットで報告する
チェック項目:
  • テストは振る舞いベースで構造化されているか
  • 各テストケースは自己完結的で、読むだけで理解できるか
  • テストコードにロジック(if/for/switch)が含まれていないか
  • モックの呼び出し回数ではなく最終状態を検証しているか
  • プライベートメソッドやフレームワーク動作をテストしていないか
  • 1 テスト 1 振る舞いになっているか
  • プロジェクトのテスト規約に準拠しているか
報告フォーマット:
undefined
Identify the test files to review from
$ARGUMENTS
and review them based on the 6 principles.
Steps:
  1. Search for target test files with
    Glob
    and read them with
    Read
  2. Also read the implementation code being tested (to confirm public API testing)
  3. Detect issues against each principle
  4. Report review results in the following format
Check Items:
  • Are tests structured based on behavior?
  • Is each test case self-contained and understandable just by reading it?
  • Does the test code contain logic (if/for/switch)?
  • Is the final state verified rather than mock call counts?
  • Are private methods or framework behaviors being tested?
  • Does each test cover only one behavior?
  • Does it comply with the project's test conventions?
Report Format:
undefined

Test Principles Review

Test Principles Review

対象: [ファイルパス]
Target: [File Path]

原則違反

Principle Violations

[原則名]

[Principle Name]

  • 箇所: filepath:line
  • 問題: 問題の説明
  • 改善案: 具体的な改善方法
  • Location: filepath:line
  • Issue: Description of the problem
  • Improvement Suggestion: Specific improvement method

総評

Overall Evaluation

  • 違反件数: [N]件
  • 改善優先度: [高/中/低]
undefined
  • Number of Violations: [N]
  • Improvement Priority: High/Medium/Low
undefined

refactor - テストのリファクタリング

refactor - Test Refactoring

$ARGUMENTS
から対象テストファイルを特定し、6 原則に基づいてリファクタリングを実行する。
手順:
  1. 対象テストファイルとテスト対象の実装コードを読み込む
  2. review と同じチェック項目で問題を特定する
  3. 以下の優先順で修正する: a. テストにロジックが入っている → 直線的なテストに展開 b. メソッドベースの構造 → 振る舞いベースに再構成 c. インタラクションテスト → 状態テストに変換 d. プライベート API テスト → 公開 API テストに変換 e. 過度に DRY なヘルパー → テストデータをインライン化 f. 1 テスト複数振る舞い → 分割
  4. 不要なテストを削除(フレームワーク動作のテスト、定数一致のみ等)
  5. テスト実行して全件パスを確認する
注意:
  • 振る舞いのカバレッジは維持する。テスト数が減ることは問題ない
  • プロジェクトのテスト規約に準拠させる
Identify target test files from
$ARGUMENTS
and refactor them based on the 6 principles.
Steps:
  1. Read the target test files and the implementation code being tested
  2. Identify issues using the same check items as review
  3. Fix in the following priority order: a. Tests with logic → Convert to linear tests b. Method-based structure → Restructure to behavior-based c. Interaction tests → Convert to state tests d. Private API tests → Convert to public API tests e. Excessively DRY helpers → Inline test data f. Single test with multiple behaviors → Split
  4. Delete unnecessary tests (framework behavior tests, only constant matches, etc.)
  5. Run tests to confirm all pass
Notes:
  • Maintain behavior coverage. A reduction in the number of tests is not a problem
  • Comply with the project's test conventions

write - テストの新規作成

write - New Test Creation

$ARGUMENTS
から対象の実装コードを特定し、6 原則に従ったテストを新規作成する。
手順:
  1. 対象の実装コードを読み込む
  2. 既存テストファイルの命名規則・配置規則を
    Glob
    で確認する
  3. 公開 API(エクスポートされた関数・メソッド)を特定する
  4. 各公開 API の振る舞いを列挙する:
    • 正常系の振る舞い
    • エッジケース(境界値、空入力、nil)
    • エラーケース
  5. 振る舞いごとにテストケースを設計する
  6. テストを記述する(table-driven test パターン必須)
  7. テスト実行して全件パスを確認する
テスト設計ガイドライン:
  • table-driven test で構造化する(
    tests := []struct{...}{...}
    パターン)
  • テスト名は日本語の Given-When-Then 形式
  • テストデータは各ケース内にインライン記述
  • 期待値はリテラルで書く
  • cmp.Diff()
    で構造体・スライス・マップを比較する
  • モックは外部依存の置き換えにのみ使う
  • 各テストは Given(準備)/ When(実行)/ Then(検証)の流れ(コメント不要、空行で区切る)
Identify the target implementation code from
$ARGUMENTS
and create new tests following the 6 principles.
Steps:
  1. Read the target implementation code
  2. Confirm naming and placement conventions of existing test files with
    Glob
  3. Identify public APIs (exported functions and methods)
  4. Enumerate the behaviors of each public API:
    • Normal behavior
    • Edge cases (boundary values, empty input, nil)
    • Error cases
  5. Design test cases for each behavior
  6. Write tests (table-driven test pattern is required)
  7. Run tests to confirm all pass
Test Design Guidelines:
  • Structure with table-driven tests (
    tests := []struct{...}{...}
    pattern)
  • Name tests in Japanese Given-When-Then format
  • Write test data inline within each case
  • Write expected values as literals
  • Compare structs/slices/maps with
    cmp.Diff()
  • Use mocks only for replacing external dependencies
  • Each test follows the flow of Given (setup) / When (execution) / Then (verification) (no comments needed, separate with blank lines)

判断基準

Judgment Criteria

テストを書くべきか迷ったときの指針:
書くべきテスト書くべきでないテスト
ビジネスロジックの振る舞い定数値の完全一致
条件分岐の各パスフレームワークの機能
エラーハンドリングgetter/setter のみのコード
データ変換ロジック外部ライブラリの動作
境界値・エッジケース実装の内部詳細
Guidelines when unsure whether to write a test:
Tests to WriteTests Not to Write
Business logic behaviorExact matches of constant values
Each path of conditional branchesFramework functions
Error handlingCode with only getters/setters
Data conversion logicBehavior of external libraries
Boundary values and edge casesInternal implementation details