golang-testing-strategies について
Go 開発者向けのテスト戦略ガイドです。標準の `testing` パッケージや Testify、Gomock を使った単体・統合テスト、ベンチマーク、並行処理の検査、カバレッジを扱います。
主な機能
- 01テーブル駆動テストとサブテスト
- 02Testify のアサーションとスイート
- 03Gomock によるインターフェースのモック
- 04ベンチマークとメモリ分析
- 05競合検出とカバレッジ
- 06CI テスト連携
活用シーン
- 01Go ライブラリやアプリのテスト作成
- 02データベースや API などの依存先のモック
- 03並行コードのデータ競合確認
- 04CI へのテストとカバレッジ導入
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill golang-testing-strategiesインストール後、Claude Code で/golang-testing-strategiesを入力して読み込まれたか確認します。
同梱ファイルmetadata.json
詳細
Go のテスト戦略
概要
Go には、シンプルさと開発者の生産性を重視した、堅牢な組み込みテストフレームワーク(testing パッケージ)が用意されています。testify や gomock などのコミュニティ製ツールと組み合わせることで、ボイラープレートを最小限に抑えながら、包括的なテストカバレッジを実現できます。
主な機能:
- 📋 テーブル駆動テスト:複数の入力をテストするための慣用的なパターンです
- ✅ Testify:読みやすいアサーションとテストスイートです
- 🎭 Gomock:型安全なインターフェイスモックです
- ⚡ ベンチマーク:組み込みのパフォーマンステストです
- 🔍 競合検出器:並行コードの安全性を検証します
- 📊 カバレッジ:ネイティブのカバレッジレポートと適用機能です
- 🚀 CI 連携:テストのキャッシュと並列実行に対応します
このスキルを使用するタイミング
次の場合にこのスキルを有効にします:
- Go ライブラリやアプリケーションのテストスイートを作成するとき
- 新しいプロジェクトのテスト基盤を設定するとき
- 外部依存関係(データベース、API、サービス)をモックするとき
- パフォーマンスが重要なコードパスのベンチマークを測定するとき
- スレッドセーフな並行実装を確認するとき
- テストを CI/CD パイプラインに統合するとき
- 他のテストフレームワークから移行するとき
テストの基本原則
Go のテスト哲学
- マジックよりシンプルさ:可能な限り標準ライブラリを使用します
- テーブル駆動テスト:1 つの関数で複数のシナリオをテストします
- サブテスト:
t.Run()で関連するテストをまとめます - インターフェイスベースのモック:インターフェイスを介して依存関係をモックします
- テストファイルをコードと同じ場所に配置:
*_test.goファイルをコードと同じディレクトリに置きます - パッケージの命名:外部テストには
package_test、内部テストにはpackageを使用します
テストの構成
ファイル名の規則:
- 単体テスト:
file_test.go - 統合テスト:
file_integration_test.go - ベンチマークテスト:同じテストファイル内で
Benchmarkを接頭辞として付けます
パッケージ構成:
mypackage/
├── user.go
├── user_test.go // Internal tests (same package)
├── user_external_test.go // External tests (package mypackage_test)
├── integration_test.go // Integration tests
└── testdata/ // Test fixtures (ignored by go build)
└── golden.json
テーブル駆動テストのパターン
基本構造
複数の入力をテストするための、Go で慣用的なテストパターンです:
func TestUserValidation(t *testing.T) {
tests := []struct {
name string
input User
wantErr bool
errMsg string
}{
{
name: "valid user",
input: User{Name: "Alice", Age: 30, Email: "[email protected]"},
wantErr: false,
},
{
name: "empty name",
input: User{Name: "", Age: 30, Email: "[email protected]"},
wantErr: true,
errMsg: "name is required",
},
{
name: "invalid email",
input: User{Name: "Bob", Age: 25, Email: "invalid"},
wantErr: true,
errMsg: "invalid email format",
},
{
name: "negative age",
input: User{Name: "Charlie", Age: -5, Email: "[email protected]"},
wantErr: true,
errMsg: "age must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}
テストの並列実行
独立したテストで並列実行を有効にします:
func TestConcurrentOperations(t *testing.T) {
tests := []struct {
name string
fn func() int
want int
}{
{"operation 1", func() int { return compute1() }, 42},
{"operation 2", func() int { return compute2() }, 84},
{"operation 3", func() int { return compute3() }, 126},
}
for _, tt := range tests {
tt := tt // Capture range variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Run tests concurrently
got := tt.fn()
if got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
Testify フレームワーク
インストール
go get github.com/stretchr/testify
アサーション
冗長なエラーチェックを、読みやすいアサーションに置き換えます:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCalculator(t *testing.T) {
calc := NewCalculator()
// assert: Test continues on failure
assert.Equal(t, 5, calc.Add(2, 3))
assert.NotNil(t, calc)
assert.True(t, calc.IsReady())
// require: Test stops on failure (for critical assertions)
result, err := calc.Divide(10, 2)
require.NoError(t, err) // Stop if error occurs
assert.Equal(t, 5, result)
}
func TestUserOperations(t *testing.T) {
user := &User{ID: 1, Name: "Alice", Email: "[email protected]"}
// Object matching
assert.Equal(t, 1, user.ID)
assert.Contains(t, user.Email, "@")
assert.Len(t, user.Name, 5)
// Partial matching
assert.ObjectsAreEqual(user, &User{
ID: 1,
Name: "Alice",
Email: assert.AnythingOfType("string"),
})
}
テストスイート
セットアップ/後処理を使って関連するテストをまとめます:
import (
"testing"
"github.com/stretchr/testify/suite"
)
type UserServiceTestSuite struct {
suite.Suite
db *sql.DB
service *UserService
}
// SetupSuite runs once before all tests
func (s *UserServiceTestSuite) SetupSuite() {
s.db = setupTestDatabase()
s.service = NewUserService(s.db)
}
// TearDownSuite runs once after all tests
func (s *UserServiceTestSuite) TearDownSuite() {
s.db.Close()
}
// SetupTest runs before each test
func (s *UserServiceTestSuite) SetupTest() {
cleanDatabase(s.db)
}
// TearDownTest runs after each test
func (s *UserServiceTestSuite) TearDownTest() {
// Cleanup if needed
}
// Test methods must start with "Test"
func (s *UserServiceTestSuite) TestCreateUser() {
user := &User{Name: "Alice", Email: "[email protected]"}
err := s.service.Create(user)
s.NoError(err)
s.NotEqual(0, user.ID) // ID assigned
}
func (s *UserServiceTestSuite) TestGetUser() {
// Setup
user := &User{Name: "Bob", Email: "[email protected]"}
s.service.Create(user)
// Test
retrieved, err := s.service.GetByID(user.ID)
s.NoError(err)
s.Equal(user.Name, retrieved.Name)
}
// Run the suite
func TestUserServiceTestSuite(t *testing.T) {
suite.Run(t, new(UserServiceTestSuite))
}
Gomock によるインターフェイスのモック
インストール
go install github.com/golang/mock/mockgen@latest
モックの生成
// user_repository.go
package repository
//go:generate mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
type UserRepository interface {
GetByID(id int) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
}
モックを生成します:
go generate ./...
# Or manually:
mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
テストでのモックの使用
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"myapp/repository/mocks"
)
func TestUserService_GetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create mock
mockRepo := mocks.NewMockUserRepository(ctrl)
// Set expectations
expectedUser := &User{ID: 1, Name: "Alice"}
mockRepo.EXPECT().
GetByID(1).
Return(expectedUser, nil).
Times(1)
// Test
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
// Assertions
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
}
func TestUserService_CreateUser_Validation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
// Expect Create to NOT be called (validation should fail first)
mockRepo.EXPECT().Create(gomock.Any()).Times(0)
service := NewUserService(mockRepo)
err := service.CreateUser(&User{Name: ""}) // Invalid user
assert.Error(t, err)
assert.Contains(t, err.Error(), "name is required")
}
カスタムマッチャー
// Custom matcher for complex validation
type userMatcher struct {
expectedEmail string
}
func (m userMatcher) Matches(x interface{}) bool {
user, ok := x.(*User)
if !ok {
return false
}
return user.Email == m.expectedEmail
}
func (m userMatcher) String() string {
return "matches user with email: " + m.expectedEmail
}
func UserWithEmail(email string) gomock.Matcher {
return userMatcher{expectedEmail: email}
}
// Usage in test
func TestCustomMatcher(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
mockRepo.EXPECT().
Create(UserWithEmail("[email protected]")).
Return(nil)
service := NewUserService(mockRepo)
service.CreateUser(&User{Name: "Alice", Email: "[email protected]"})
}
ベンチマークテスト
基本的なベンチマーク
func BenchmarkAdd(b *testing.B) {
calc := NewCalculator()
for i := 0; i < b.N; i++ {
calc.Add(2, 3)
}
}
func BenchmarkStringConcatenation(b *testing.B) {
b.Run("plus operator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + "world"
}
})
b.Run("strings.Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString("world")
_ = sb.String()
}
})
}
ベンチマークの実行
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkAdd
# With memory allocation stats
go test -bench=. -benchmem
# Compare benchmarks
go test -bench=. -benchmem > old.txt
# Make changes
go test -bench=. -benchmem > new.txt
benchstat old.txt new.txt
ベンチマーク出力の例
BenchmarkAdd-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op
BenchmarkStringBuilder-8 50000000 28.5 ns/op 64 B/op 1 allocs/op
読み方:50000000 回の反復、1 操作あたり 28.5 ns/op、1 操作あたり 64 B/op のバイト数を割り当て、1 操作あたり 1 allocs/op 回の割り当て
高度なテストパターン
HTTP ハンドラー向けの httptest
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestUserHandler(t *testing.T) {
handler := http.HandlerFunc(UserHandler)
req := httptest.NewRequest("GET", "/users/1", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Alice")
}
func TestHTTPClient(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/users", r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id": 1, "name": "Alice"}`))
}))
defer server.Close()
// Test client against mock server
client := NewAPIClient(server.URL)
user, err := client.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
競合検出器
並行コード内のデータ競合を検出します:
go test -race ./...
並行安全性を確認するテストの例:
func TestConcurrentMapAccess(t *testing.T) {
cache := NewSafeCache()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
cache.Set(fmt.Sprintf("key%d", val), val)
}(i)
}
wg.Wait()
assert.Equal(t, 100, cache.Len())
}
ゴールデンファイルテスト
期待される出力ファイルと照合してテストします:
func TestRenderTemplate(t *testing.T) {
output := RenderTemplate("user", User{Name: "Alice"})
goldenFile := "testdata/user_template.golden"
if *update {
// Update golden file: go test -update
os.WriteFile(goldenFile, []byte(output), 0644)
}
expected, err := os.ReadFile(goldenFile)
require.NoError(t, err)
assert.Equal(t, string(expected), output)
}
var update = flag.Bool("update", false, "update golden files")
CI/CD 連携
GitHub Actions の例
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
- name: Check coverage threshold
run: |
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
awk '{if ($1 < 80) exit 1}'
カバレッジの適用
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View coverage in terminal
go tool cover -func=coverage.out
# Generate HTML report
go tool cover -html=coverage.out -o coverage.html
# Check coverage threshold (fail if < 80%)
go test -coverprofile=coverage.out ./... && \
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'
意思決定ツリー
各テストツールの使いどころ
標準の testing パッケージを使う場合:
- アサーションが少ないシンプルなユニットテスト
- モック化する外部依存がない
- パフォーマンスのベンチマーク
- 依存関係を最小限に抑えたい
Testify を使う場合:
- 読みやすいアサーションが必要(冗長なチェックより
assert.Equal) - セットアップ/ティアダウンを伴うテストスイート
- 似たテストケースが複数ある
- 表現力の高いテストコードを使いたい
Gomock を使う場合:
- インターフェースに依存するコードのテスト
- 呼び出しの回数や順序を正確に検証したい
- 複数のシナリオを含む複雑なモック動作
- 型安全なモックが必要
ベンチマークを使う場合:
- パフォーマンスが重要なコードを最適化する
- アルゴリズムの実装を比較する
- パフォーマンスのリグレッションを検出する
- メモリ割り当てをプロファイリングする
httptest を使う場合:
- HTTP ハンドラーをテストする
- 外部 HTTP API をモック化する
- HTTP クライアントを統合テストする
- ミドルウェアチェーンをテストする
レースディテクターを使う場合:
- 並行コードを書く
- ゴルーチンやチャネルを使う
- ゴルーチン間で状態を共有する
- 並行コードすべてに対して CI/CD で実行する
避けるべきアンチパターン
❌ すべてをモック化しない
// WRONG: Over-mocking makes tests brittle
mockLogger := mocks.NewMockLogger(ctrl)
mockConfig := mocks.NewMockConfig(ctrl)
mockMetrics := mocks.NewMockMetrics(ctrl)
// Too many mocks = fragile test
✅ 外部依存のみをモック化する
// CORRECT: Mock only database, use real logger/config
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo, realLogger, realConfig)
❌ 実装の詳細をテストしない
// WRONG: Testing internal state
assert.Equal(t, "processing", service.internalState)
✅ 公開されている振る舞いをテストする
// CORRECT: Test observable outcomes
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
❌ エラーケースを無視しない
// WRONG: Only testing happy path
func TestGetUser(t *testing.T) {
user, _ := service.GetUser(1) // Ignoring error!
assert.NotNil(t, user)
}
✅ エラー条件をテストする
// CORRECT: Test both success and error cases
func TestGetUser_NotFound(t *testing.T) {
user, err := service.GetUser(999)
assert.Error(t, err)
assert.Nil(t, user)
assert.Contains(t, err.Error(), "not found")
}
ベストプラクティス
- テストをソースコードと同じ場所に配置する:
*_test.goファイルをソースコードと同じディレクトリに置きます - サブテストを使う:
t.Run()で関連するテストを整理します - 安全な場合は並列実行する:独立したテストでは
t.Parallel()を有効にします - インターフェースをモック化する:インターフェースへの依存を活用し、テストしやすい設計にします
- エラーをテストする:成功パスと失敗パスの両方を検証します
- 重要な処理をベンチマークする:パフォーマンスに影響するコードをプロファイリングします
- レースディテクターを実行する:並行コードでは必ず
-raceを使います - カバレッジを確保する:CI で最低しきい値を設定します(通常は 80%)
- ゴールデンファイルを使う:複雑な出力を期待されるファイルと比較してテストします
- テストを高速に保つ:時間のかかる処理をモック化し、素早く実行する場合は
-shortフラグを使います
リソース
公式ドキュメント:
- Go Testing パッケージ:https://pkg.go.dev/testing
- テーブル駆動テスト:https://github.com/golang/go/wiki/TableDrivenTests
- サブテストとサブベンチマーク:https://go.dev/blog/subtests
テストフレームワーク:
- Testify:https://github.com/stretchr/testify
- Gomock:https://github.com/golang/mock
- httptest:https://pkg.go.dev/net/http/httptest
最近のガイド(2025 年):
- 「Go Unit Testing: Structure & Best Practices」(2025 年 11 月)
- Go Wiki:テストにおけるよくある間違い
- Google Go Style Guide - Testing:https://google.github.io/styleguide/go/
関連スキル:
- golang-engineer:Go の基本パターンと並行処理
- verification-before-completion:「完了」の一部としてのテスト
- testing-anti-patterns:よくあるテストの間違いを避ける
クイックリファレンス
テストを実行する
go test ./... # All tests
go test -v ./... # Verbose output
go test -short ./... # Skip slow tests
go test -run TestUserCreate # Specific test
go test -race ./... # With race detector
go test -cover ./... # With coverage
go test -coverprofile=c.out ./... # Coverage file
go test -bench=. -benchmem # Benchmarks with memory
モックを生成する
go generate ./... # All //go:generate directives
mockgen -source=interface.go -destination=mock.go
カバレッジを分析する
go tool cover -func=coverage.out # Coverage per function
go tool cover -html=coverage.out # HTML report
トークンの推定数:約 4,500 トークン(エントリーポイント+全コンテンツ) バージョン:1.0.0 最終更新日:2025-12-03