mcpField
G
go-test-quality

go-test-quality

github.com/eduardo-slテスト・品質83198 回インストール2 週間前に更新
リポジトリ

go-test-quality について

go-test-qualityは、Go開発者やコーディングエージェント向けのテストスキルです。動作を検証するテスト、保守しやすいフィクスチャ、並行処理の安全性を重視し、テスト構成からモック、競合チェック、ベンチマークまで扱います。

主な機能

  1. 01動作を重視したテスト設計
  2. 02サブテスト、命名、ファイル構成
  3. 03ヘルパー、フィクスチャ、クリーンアップの指針
  4. 04モックとテスト種類の選択
  5. 05並行処理、カバレッジ、ベンチマークの実践

活用シーン

  1. 01Goの動作や境界条件を検証するテスト設計
  2. 02HTTPハンドラーやデータベース動作のテスト
  3. 03並行テストの不安定さやリークの確認

インストール

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

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-test-quality

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
concurrency-and-benchmarks.md
helpers-and-fixtures.md
integration-testing.md

詳細

Go テストの品質

テストは本番コードです。すべてのコミットで CI 上で実行され、動作を文書化し、午前3時に関数が壊れたときに最初に読むものです。 金銭を扱うコードと同じくらい注意深く書いてください。

コミュニティのデフォルトです。go-test-quality を明示的に置き換えるプロジェクトのスキルまたはエージェント設定ルールがある場合は、そちらが優先されます。

詳細なリファレンス資料は、必要に応じて読み込みます。

  • references/helpers-and-fixtures.md — テストヘルパー、オプション付きファクトリー関数、 t.Cleanup、ゴールデンファイル、モック実装。
  • references/integration-testing.md — httptest レコーダーとサーバー、 testcontainers、ビルドタグ、TestMain、ファズテスト。
  • references/concurrency-and-benchmarks.md — synctest、goleak、 t.Context()、b.Loop() ベンチマーク、t.Attr とアーティファクト。

以下の概要だけでは不十分な場合に限り、リファレンスファイルを読んでください。

1. テスト設計の基本方針

実装ではなく動作をテストする
// ✅ Good — tests what the function DOES
func TestTransferFunds_InsufficientBalance(t *testing.T) {
    from := NewAccount("alice", 100)
    to := NewAccount("bob", 0)

    err := TransferFunds(from, to, 150)

    require.ErrorIs(t, err, ErrInsufficientFunds)
    assert.Equal(t, 100, from.Balance(), "sender balance should be unchanged")
    assert.Equal(t, 0, to.Balance(), "receiver balance should be unchanged")
}

// ❌ Bad — tests HOW the function does it
// asserts debit() was called before credit(), rollback() was called,
// internal mutex was locked — breaks on every refactor
論理的な概念ごとにアサーションを1つにする

同じ動作の異なる側面(送金後の両方の口座など)を検証する場合は、複数の assert 呼び出しを使ってもかまいません。作成、更新、削除を確認するテストは、1つに見せかけた3つのテストです。

バグ報告のような名前をテストに付ける

テストが失敗したとき、名前だけで何が壊れたのかが分かるようにします。

// ✅ Good — reads like a sentence
func TestOrderService_Cancel_RefundsPartiallyShippedItems(t *testing.T) { ... }
func TestParseConfig_ReturnsErrorOnMissingRequiredField(t *testing.T) { ... }

// ❌ Bad — says nothing useful
func TestCancel(t *testing.T) { ... }
func TestRateLimiter_Success(t *testing.T) { ... }
構成
  • ソースファイルごとに _test.go を1つ用意します(parser.go → parser_test.go)。テストは対象関数の順に並べます。ツールやレビュー担当者はファイル単位で確認します。
  • 公開 API のテストは pkg_test パッケージに置き、呼び出し側が使えるものだけを使用できるようにします。ホワイトボックステストは pkg に置きます。
  • 統合テストは //go:build integration の後ろに置きます。

2. シナリオを整理するサブテスト

t.Run を使って、関連するシナリオを親テストの下にまとめます。各サブテストには独自のセットアップ、失敗結果、CI 出力上の名前が割り当てられます。

func TestUserService_Create(t *testing.T) {
    svc := setupUserService(t)

    t.Run("succeeds with valid input", func(t *testing.T) {
        user, err := svc.Create(ctx, CreateUserInput{Name: "Alice", Email: "[email protected]"})
        require.NoError(t, err)
        assert.NotEmpty(t, user.ID)
    })

    t.Run("rejects duplicate email", func(t *testing.T) {
        _, _ = svc.Create(ctx, CreateUserInput{Name: "Alice", Email: "[email protected]"})
        _, err := svc.Create(ctx, CreateUserInput{Name: "Bob", Email: "[email protected]"})
        require.ErrorIs(t, err, ErrDuplicateEmail)
    })
}

3. テストヘルパーのルール

  1. テストユーティリティでは必ず t.Helper() を呼び出してください。 失敗箇所がヘルパーではなく呼び出し元を示すようになります。
  2. 複雑なテストオブジェクトには、関数型オプションを使ったファクトリー関数を使用してください。 テストごとに上書きできるデフォルト値を用意し、15個のパラメーターを取るコンストラクターは使いません。
  3. defer より t.Cleanup を優先してください。 t.FailNow() の後でも実行され、関数ではなくテストのスコープに紐づきます。

実装の全体は references/helpers-and-fixtures.md を参照してください。

4. テストの種類を選ぶ

状況アプローチ詳細
純粋関数でデータケースが3つ以上テーブル駆動テストgo-test-table-driven スキル
分離された HTTP ハンドラーhttptest.NewRecorder + モックストアreferences/integration-testing.md
ルーティング/ミドルウェアの全スタックhttptest.NewServerreferences/integration-testing.md
実際のデータベース動作testcontainers + ビルドタグreferences/integration-testing.md
複雑な出力(JSON、HTML、SQL)testdata/ のゴールデンファイルreferences/helpers-and-fixtures.md
信頼できない入力を扱うパーサー/バリデーターファズテストreferences/integration-testing.md

5. モックのルール

  • 小さなインターフェース(メソッド数が3以下)には、インターフェースベースの手書きモックを使います。 関数フィールドと呼び出し記録を持つ構造体を用意します。
  • 単純な差し替えポイントには関数の注入を使います(now func() time.Time など)。
  • モック化しないでください。 値オブジェクト、純粋関数、標準ライブラリ、同じパッケージ内の自分のコードは対象外です。実物をテストしてください。
  • すべてをモック化すると、テスト対象はコードではなくモックになります。

6. 並列実行とカバレッジ

func TestSlugify(t *testing.T) {
    t.Parallel() // safe: pure function, no shared state
    // ...
}

テスト間で可変状態、データベース、ファイル、またはプロセスレベルの状態を共有する場合は、t.Parallel() を使わないでください。まさにその理由で、並列テスト内では t.Setenv と t.Chdir がパニックを起こします。

go test -race -coverprofile=coverage.out -coverpkg=./... ./...
go tool cover -html=coverage.out   # read the red lines, not the percentage

カバレッジで分かるのは未テストのコードであり、アサーションの品質ではありません。カバレッジを使って不足箇所を見つけ、CI では引き上げる一方の下限を適用してください(ゲートは go-ci スキルが管理します)。-coverpkg を指定しない場合、別のパッケージのテストからのみ実行されるパッケージのカバレッジは 0% と報告されます。

7. 並行コードのテスト

  • テストの同期に time.Sleep を使ってはいけません。うまく動いても遅く、うまく動かないと不安定になります。
  • Go 1.25 以降:時間に依存するコードは synctest.Test の中で実行します。バブル内のフェイククロックは、すべてのゴルーチンがブロックされると即座に進み、synctest.Wait() は「バックグラウンドのゴルーチンがそこに到達するまでスリープする」処理の代わりになります。Go 1.24 では、GOEXPERIMENT=synctest を設定したうえで synctest.Run を使います。Go 1.27 では、時間の進行と安定化を一度の呼び出しで行う synctest.Sleep(d) と、バブル内で動作して自動的に閉じるインメモリサーバー httptest.NewTestServer(t, h) が追加されます。
  • ゴルーチンを起動するパッケージでは、ゴルーチンのリークがないことを検証します。TestMain で goleak.VerifyTestMain(m) を使うか、defer goleak.VerifyNone(t) を使います。-race はデータ競合を検出しますが、終了しないゴルーチンは検出しません。

8. テストのコンテキスト、ベンチマーク、成果物

  • context.Background() の代わりに t.Context()(Go 1.24 以降)を使います。これは t.Cleanup の実行前にキャンセルされるため、テストが終わるとブロック中の処理が巻き戻されます。
  • ベンチマークでは for b.Loop()(Go 1.24 以降)を使います。セットアップは一度だけ実行され、シンクがなくても結果は生存し続けます。b.ReportAllocs() を追加し、benchstat で実行結果を比較します。
  • t.Attr(Go 1.25 以降)は、CI が解析できるキーと値のペアを出力します。go test -artifacts と併せて t.ArtifactDir()(Go 1.26 以降)を使うと、失敗したゴールデン出力やキャプチャした本文をリポジトリの外に保存できます。

両セクションの例:references/concurrency-and-benchmarks.md。

9. アサーションライブラリの落とし穴

  • 各 t.Run の中で assert.New(t)/require.New(t) を作成します。 親の t から作成したインスタンスは、サブテストの失敗を親に報告し、失敗したサブテスト自体は PASS と表示します。
  • require、t.Fatal、t.FailNow はテストのゴルーチン内でのみ使います。 起動したゴルーチンから呼び出しても、テストを停止できません。結果をチャネルで返し、受信後にアサーションします。go vet の testinggoroutine と golangci-lint の testifylint で、ほとんどのケースを検出できます。
  • 前提条件には require、独立したチェックには assert を使います。前提条件が満たされない場合、nil に対するパニックが発生する前にテストを停止する必要があります。
  • assert.Eventually はポーリングのタイミングに関するバグを隠します。代わりに synctest を使います。

反パターン

  • 🔴 アサーションのないテスト — 常に成功し、何も証明しません
  • 🔴 同期に time.Sleep を使う — testing/synctest、チャネル、またはポーリングを使います
  • 🔴 実行順序に依存するテスト — 各テストは単独で実行できる必要があります
  • 🔴 すべてをモック化する — コードではなく、モックをテストすることになります
  • 🟡 Test1、TestSuccess のようなテスト名 — シナリオを名前にします
  • 🟡 非公開フィールドに直接アクセスする — 公開 API を通じてテストします
  • 🟡 エッジケースがない:空、nil、ゼロ、最大値、Unicode
  • 🟡 巨大な共通セットアップ — 各テストでは必要なものだけをセットアップします
  • 🟡 テストで context.Background() を使う — t.Context() を使います
  • 🟡 Go 1.24 以降で for range b.N を使う — for b.Loop() を使います
  • 🟢 信頼できない入力を受け取るものはすべてファズテストします
  • 🟢 複雑な出力の比較にはゴールデンファイルを使います

検証チェックリスト

  1. すべてのテストに意味のあるアサーションがある(テスト本体が空ではない)
  2. テスト名はメソッドではなくシナリオを説明している
  3. すべてのテストユーティリティ関数で t.Helper() を呼び出している
  4. リソースの後処理に t.Cleanup() を使っている
  5. 安全な場合は t.Parallel() を使い、安全でない場合は使わない
  6. 統合テストを testing.Short() またはビルドタグで制御している
  7. モックは最小限にし、外部依存関係のみをモック化している
  8. テストスイート内のどこにも同期目的の time.Sleep がない
  9. ゴルーチンを起動するパッケージで goleak による検証を行っている
  10. ベンチマークで for b.Loop() を使い、b.ReportAllocs() を呼び出している
  11. エッジケースを網羅している:空、nil、ゼロ、境界値
  12. go test -race ./... が成功する
  13. カバレッジを使って不足箇所を見つけ、CI では目標値ではなく段階的に引き上げる下限値を適用している
  14. testify のインスタンスをサブテストごとに作成し、テストのゴルーチン外で require を使っていない

コメント