go-test-quality について
go-test-qualityは、Go開発者やコーディングエージェント向けのテストスキルです。動作を検証するテスト、保守しやすいフィクスチャ、並行処理の安全性を重視し、テスト構成からモック、競合チェック、ベンチマークまで扱います。
主な機能
- 01動作を重視したテスト設計
- 02サブテスト、命名、ファイル構成
- 03ヘルパー、フィクスチャ、クリーンアップの指針
- 04モックとテスト種類の選択
- 05並行処理、カバレッジ、ベンチマークの実践
活用シーン
- 01Goの動作や境界条件を検証するテスト設計
- 02HTTPハンドラーやデータベース動作のテスト
- 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.mdhelpers-and-fixtures.mdintegration-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. テストヘルパーのルール
- テストユーティリティでは必ず
t.Helper()を呼び出してください。 失敗箇所がヘルパーではなく呼び出し元を示すようになります。 - 複雑なテストオブジェクトには、関数型オプションを使ったファクトリー関数を使用してください。 テストごとに上書きできるデフォルト値を用意し、15個のパラメーターを取るコンストラクターは使いません。
deferよりt.Cleanupを優先してください。t.FailNow()の後でも実行され、関数ではなくテストのスコープに紐づきます。
実装の全体は references/helpers-and-fixtures.md を参照してください。
4. テストの種類を選ぶ
| 状況 | アプローチ | 詳細 |
|---|---|---|
| 純粋関数でデータケースが3つ以上 | テーブル駆動テスト | go-test-table-driven スキル |
| 分離された HTTP ハンドラー | httptest.NewRecorder + モックストア | references/integration-testing.md |
| ルーティング/ミドルウェアの全スタック | httptest.NewServer | references/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()を使います - 🟢 信頼できない入力を受け取るものはすべてファズテストします
- 🟢 複雑な出力の比較にはゴールデンファイルを使います
検証チェックリスト
- すべてのテストに意味のあるアサーションがある(テスト本体が空ではない)
- テスト名はメソッドではなくシナリオを説明している
- すべてのテストユーティリティ関数で
t.Helper()を呼び出している - リソースの後処理に
t.Cleanup()を使っている - 安全な場合は
t.Parallel()を使い、安全でない場合は使わない - 統合テストを
testing.Short()またはビルドタグで制御している - モックは最小限にし、外部依存関係のみをモック化している
- テストスイート内のどこにも同期目的の
time.Sleepがない - ゴルーチンを起動するパッケージで
goleakによる検証を行っている - ベンチマークで
for b.Loop()を使い、b.ReportAllocs()を呼び出している - エッジケースを網羅している:空、nil、ゼロ、境界値
go test -race ./...が成功する- カバレッジを使って不足箇所を見つけ、CI では目標値ではなく段階的に引き上げる下限値を適用している
- testify のインスタンスをサブテストごとに作成し、テストのゴルーチン外で
requireを使っていない