Golang Testing

affaan-m/ECC/docs/ja-JP/skills/golang-testing

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775無授權條款275K 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫4 天前更新

テスト駆動開発とGoコードの高品質を保証するための包括的なテスト戦略。

AI 產生的概覽

針對 Go 程式碼的完整測試策略,涵蓋 TDD、表驅動測試、模擬、基準測試、模糊測試與覆蓋率。

功能
此技能提供測試 Go 程式碼的指引與程式碼模式,遵循測試驅動開發流程。內容涵蓋表驅動測試、子測試、斷言與輔助函式、以介面為基礎的模擬、HTTP 處理常式與資料庫測試、基準測試、模糊測試、覆蓋率、整合測試與平行測試。它也列出 Go 測試指令以及應避免的常見反模式。
適用情境
在撰寫新的 Go 程式碼、審查 Go 程式碼、改善現有測試、提升測試覆蓋率,或偵錯與修正 Go 專案缺陷時使用。
執行需求
不隨附指令碼,僅為說明性內容。實作範例需要 Go 工具鏈;部分模式還需要外部套件(例如 testcontainers-go)以及資料庫或容器執行環境。

Go テスト

テスト駆動開発(TDD)とGoコードの高品質を保証するための包括的なテスト戦略。

いつ有効化するか

  • 新しいGoコードを書くとき
  • Goコードをレビューするとき
  • 既存のテストを改善するとき
  • テストカバレッジを向上させるとき
  • デバッグとバグ修正時

核となる原則

1. テスト駆動開発(TDD)ワークフロー

失敗するテストを書き、実装し、リファクタリングするサイクルに従います。

go
// 1. テストを書く(失敗)func TestCalculateTotal(t *testing.T) {    total := CalculateTotal([]float64{10.0, 20.0, 30.0})    want := 60.0    if total != want {        t.Errorf("got %f, want %f", total, want)    }}
// 2. 実装する(テストを通す)func CalculateTotal(prices []float64) float64 {    var total float64    for _, price := range prices {        total += price    }    return total}
// 3. リファクタリング// テストを壊さずにコードを改善

2. テーブル駆動テスト

複数のケースを体系的にテストします。

go
func TestAdd(t *testing.T) {    tests := []struct {        name string        a, b int        want int    }{        {"positive numbers", 2, 3, 5},        {"negative numbers", -2, -3, -5},        {"mixed signs", -2, 3, 1},        {"zeros", 0, 0, 0},    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            got := Add(tt.a, tt.b)            if got != tt.want {                t.Errorf("Add(%d, %d) = %d; want %d",                    tt.a, tt.b, got, tt.want)            }        })    }}

3. サブテスト

サブテストを使用した論理的なテストの構成。

go
func TestUser(t *testing.T) {    t.Run("validation", func(t *testing.T) {        t.Run("empty email", func(t *testing.T) {            user := User{Email: ""}            if err := user.Validate(); err == nil {                t.Error("expected validation error")            }        })
        t.Run("valid email", func(t *testing.T) {            user := User{Email: "[email protected]"}            if err := user.Validate(); err != nil {                t.Errorf("unexpected error: %v", err)            }        })    })
    t.Run("serialization", func(t *testing.T) {        // 別のテストグループ    })}

テスト構成

ファイル構成

text
mypackage/├── user.go├── user_test.go          # ユニットテスト├── integration_test.go   # 統合テスト├── testdata/             # テストフィクスチャ│   ├── valid_user.json│   └── invalid_user.json└── export_test.go        # 内部テスト用の非公開エクスポート

テストパッケージ

go
// user_test.go - 同じパッケージ(ホワイトボックステスト)package user
func TestInternalFunction(t *testing.T) {    // 内部をテストできる}
// user_external_test.go - 外部パッケージ(ブラックボックステスト)package user_test
import "myapp/user"
func TestPublicAPI(t *testing.T) {    // 公開APIのみをテスト}

アサーションとヘルパー

基本的なアサーション

go
func TestBasicAssertions(t *testing.T) {    // 等価性    got := Calculate()    want := 42    if got != want {        t.Errorf("got %d, want %d", got, want)    }
    // エラーチェック    _, err := Process()    if err != nil {        t.Fatalf("unexpected error: %v", err)    }
    // nil チェック    result := GetResult()    if result == nil {        t.Fatal("expected non-nil result")    }}

カスタムヘルパー関数

go
// ヘルパーとしてマーク(スタックトレースに表示されない)func assertEqual(t *testing.T, got, want interface{}) {    t.Helper()    if got != want {        t.Errorf("got %v, want %v", got, want)    }}
func assertNoError(t *testing.T, err error) {    t.Helper()    if err != nil {        t.Fatalf("unexpected error: %v", err)    }}
// 使用例func TestWithHelpers(t *testing.T) {    result, err := Process()    assertNoError(t, err)    assertEqual(t, result.Status, "success")}

ディープ等価性チェック

go
import "reflect"
func assertDeepEqual(t *testing.T, got, want interface{}) {    t.Helper()    if !reflect.DeepEqual(got, want) {        t.Errorf("got %+v, want %+v", got, want)    }}
func TestStructEquality(t *testing.T) {    got := User{Name: "Alice", Age: 30}    want := User{Name: "Alice", Age: 30}    assertDeepEqual(t, got, want)}

モッキングとスタブ

インターフェースベースのモック

go
// 本番コードtype UserStore interface {    GetUser(id string) (*User, error)    SaveUser(user *User) error}
type UserService struct {    store UserStore}
// テストコードtype MockUserStore struct {    users map[string]*User    err   error}
func (m *MockUserStore) GetUser(id string) (*User, error) {    if m.err != nil {        return nil, m.err    }    return m.users[id], nil}
func (m *MockUserStore) SaveUser(user *User) error {    if m.err != nil {        return m.err    }    m.users[user.ID] = user    return nil}
// テストfunc TestUserService(t *testing.T) {    mock := &MockUserStore{        users: make(map[string]*User),    }    service := &UserService{store: mock}
    // サービスをテスト...}

時間のモック

go
// プロダクションコード - 時間を注入可能にするtype TimeProvider interface {    Now() time.Time}
type RealTime struct{}
func (RealTime) Now() time.Time {    return time.Now()}
type Service struct {    time TimeProvider}
// テストコードtype MockTime struct {    current time.Time}
func (m MockTime) Now() time.Time {    return m.current}
func TestTimeDependent(t *testing.T) {    mockTime := MockTime{        current: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),    }    service := &Service{time: mockTime}
    // 固定時間でテスト...}

HTTP クライアントのモック

go
type HTTPClient interface {    Do(req *http.Request) (*http.Response, error)}
type MockHTTPClient struct {    response *http.Response    err      error}
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {    return m.response, m.err}
func TestAPICall(t *testing.T) {    mockClient := &MockHTTPClient{        response: &http.Response{            StatusCode: 200,            Body:       io.NopCloser(strings.NewReader(`{"status":"ok"}`)),        },    }
    api := &APIClient{client: mockClient}    // APIクライアントをテスト...}

HTTPハンドラーのテスト

httptest の使用

go
func TestHandler(t *testing.T) {    handler := http.HandlerFunc(MyHandler)
    req := httptest.NewRequest("GET", "/users/123", nil)    rec := httptest.NewRecorder()
    handler.ServeHTTP(rec, req)
    // ステータスコードをチェック    if rec.Code != http.StatusOK {        t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)    }
    // レスポンスボディをチェック    var response map[string]interface{}    if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {        t.Fatalf("failed to decode response: %v", err)    }
    if response["id"] != "123" {        t.Errorf("got id %v, want 123", response["id"])    }}

ミドルウェアのテスト

go
func TestAuthMiddleware(t *testing.T) {    // ダミーハンドラー    nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {        w.WriteHeader(http.StatusOK)    })
    // ミドルウェアでラップ    handler := AuthMiddleware(nextHandler)
    tests := []struct {        name       string        token      string        wantStatus int    }{        {"valid token", "valid-token", http.StatusOK},        {"invalid token", "invalid", http.StatusUnauthorized},        {"no token", "", http.StatusUnauthorized},    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            req := httptest.NewRequest("GET", "/", nil)            if tt.token != "" {                req.Header.Set("Authorization", "Bearer "+tt.token)            }            rec := httptest.NewRecorder()
            handler.ServeHTTP(rec, req)
            if rec.Code != tt.wantStatus {                t.Errorf("got status %d, want %d", rec.Code, tt.wantStatus)            }        })    }}

テストサーバー

go
func TestAPIIntegration(t *testing.T) {    // テストサーバーを作成    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {        json.NewEncoder(w).Encode(map[string]string{            "message": "hello",        })    }))    defer server.Close()
    // 実際のHTTPリクエストを行う    resp, err := http.Get(server.URL)    if err != nil {        t.Fatalf("request failed: %v", err)    }    defer resp.Body.Close()
    // レスポンスを検証    var result map[string]string    json.NewDecoder(resp.Body).Decode(&result)
    if result["message"] != "hello" {        t.Errorf("got %s, want hello", result["message"])    }}

データベーステスト

トランザクションを使用したテストの分離

go
func TestUserRepository(t *testing.T) {    db := setupTestDB(t)    defer db.Close()
    tests := []struct {        name string        fn   func(*testing.T, *sql.DB)    }{        {"create user", testCreateUser},        {"find user", testFindUser},        {"update user", testUpdateUser},    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            tx, err := db.Begin()            if err != nil {                t.Fatal(err)            }            defer tx.Rollback() // テスト後にロールバック
            tt.fn(t, tx)        })    }}

テストフィクスチャ

go
func setupTestDB(t *testing.T) *sql.DB {    t.Helper()
    db, err := sql.Open("postgres", "postgres://localhost/test")    if err != nil {        t.Fatalf("failed to connect: %v", err)    }
    // スキーマを移行    if err := runMigrations(db); err != nil {        t.Fatalf("migrations failed: %v", err)    }
    return db}
func seedTestData(t *testing.T, db *sql.DB) {    t.Helper()
    fixtures := []string{        `INSERT INTO users (id, email) VALUES ('1', '[email protected]')`,        `INSERT INTO posts (id, user_id, title) VALUES ('1', '1', 'Test Post')`,    }
    for _, query := range fixtures {        if _, err := db.Exec(query); err != nil {            t.Fatalf("failed to seed data: %v", err)        }    }}

ベンチマーク

基本的なベンチマーク

go
func BenchmarkCalculation(b *testing.B) {    for i := 0; i < b.N; i++ {        Calculate(100)    }}
// メモリ割り当てを報告func BenchmarkWithAllocs(b *testing.B) {    b.ReportAllocs()    for i := 0; i < b.N; i++ {        ProcessData([]byte("test data"))    }}

サブベンチマーク

go
func BenchmarkEncoding(b *testing.B) {    data := generateTestData()
    b.Run("json", func(b *testing.B) {        b.ReportAllocs()        for i := 0; i < b.N; i++ {            json.Marshal(data)        }    })
    b.Run("gob", func(b *testing.B) {        b.ReportAllocs()        var buf bytes.Buffer        enc := gob.NewEncoder(&buf)        b.ResetTimer()        for i := 0; i < b.N; i++ {            enc.Encode(data)            buf.Reset()        }    })}

ベンチマーク比較

go
// 実行: go test -bench=. -benchmemfunc BenchmarkStringConcat(b *testing.B) {    b.Run("operator", func(b *testing.B) {        for i := 0; i < b.N; i++ {            _ = "hello" + " " + "world"        }    })
    b.Run("fmt.Sprintf", func(b *testing.B) {        for i := 0; i < b.N; i++ {            _ = fmt.Sprintf("%s %s", "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(" ")            sb.WriteString("world")            _ = sb.String()        }    })}

ファジングテスト

基本的なファズテスト(Go 1.18+)

go
func FuzzParseInput(f *testing.F) {    // シードコーパス    f.Add("hello")    f.Add("world")    f.Add("123")
    f.Fuzz(func(t *testing.T, input string) {        // パースがパニックしないことを確認        result, err := ParseInput(input)
        // エラーがあっても、nilでないか一貫性があることを確認        if err == nil && result == nil {            t.Error("got nil result with no error")        }    })}

より複雑なファジング

go
func FuzzJSONParsing(f *testing.F) {    f.Add([]byte(`{"name":"test","age":30}`))    f.Add([]byte(`{"name":"","age":0}`))
    f.Fuzz(func(t *testing.T, data []byte) {        var user User        err := json.Unmarshal(data, &user)
        // JSONがデコードされる場合、再度エンコードできるべき        if err == nil {            _, err := json.Marshal(user)            if err != nil {                t.Errorf("marshal failed after successful unmarshal: %v", err)            }        }    })}

テストカバレッジ

カバレッジの実行と表示

bash
# カバレッジを実行してHTMLレポートを生成go test -coverprofile=coverage.out ./...go tool cover -html=coverage.out -o coverage.html
# パッケージごとのカバレッジを表示go test -cover ./...
# 詳細なカバレッジgo test -coverprofile=coverage.out -covermode=atomic ./...

カバレッジのベストプラクティス

go
// Good: テスタブルなコードfunc ProcessData(data []byte) (Result, error) {    if len(data) == 0 {        return Result{}, ErrEmptyData    }
    // 各分岐をテスト可能    if isValid(data) {        return parseValid(data)    }    return parseInvalid(data)}
// 対応するテストが全分岐をカバーfunc TestProcessData(t *testing.T) {    tests := []struct {        name    string        data    []byte        wantErr bool    }{        {"empty data", []byte{}, true},        {"valid data", []byte("valid"), false},        {"invalid data", []byte("invalid"), false},    }    // ...}

統合テスト

ビルドタグの使用

go
//go:build integration// +build integration
package myapp_test
import "testing"
func TestDatabaseIntegration(t *testing.T) {    // 実際のDBを必要とするテスト}
bash
# 統合テストを実行go test -tags=integration ./...
# 統合テストを除外go test ./...

テストコンテナの使用

go
import "github.com/testcontainers/testcontainers-go"
func setupPostgres(t *testing.T) *sql.DB {    ctx := context.Background()
    req := testcontainers.ContainerRequest{        Image:        "postgres:15",        ExposedPorts: []string{"5432/tcp"},        Env: map[string]string{            "POSTGRES_PASSWORD": "test",            "POSTGRES_DB":       "testdb",        },    }
    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{        ContainerRequest: req,        Started:          true,    })    if err != nil {        t.Fatal(err)    }
    t.Cleanup(func() {        container.Terminate(ctx)    })
    // コンテナに接続    // ...    return db}

テストの並列化

並列テスト

go
func TestParallel(t *testing.T) {    tests := []struct {        name string        fn   func(*testing.T)    }{        {"test1", testCase1},        {"test2", testCase2},        {"test3", testCase3},    }
    for _, tt := range tests {        tt := tt // ループ変数をキャプチャ        t.Run(tt.name, func(t *testing.T) {            t.Parallel() // このテストを並列実行            tt.fn(t)        })    }}

並列実行の制御

go
func TestWithResourceLimit(t *testing.T) {    // 同時に5つのテストのみ    sem := make(chan struct{}, 5)
    tests := generateManyTests()
    for _, tt := range tests {        tt := tt        t.Run(tt.name, func(t *testing.T) {            t.Parallel()
            sem <- struct{}{}        // 獲得            defer func() { <-sem }() // 解放
            tt.fn(t)        })    }}

Goツール統合

テストコマンド

bash
# 基本テストgo test ./...go test -v ./...                    # 詳細出力go test -run TestSpecific ./...     # 特定のテストを実行
# カバレッジgo test -cover ./...go test -coverprofile=coverage.out ./...
# レースコンディションgo test -race ./...
# ベンチマークgo test -bench=. ./...go test -bench=. -benchmem ./...go test -bench=. -cpuprofile=cpu.prof ./...
# ファジングgo test -fuzz=FuzzTest
# 統合テストgo test -tags=integration ./...
# JSONフォーマット(CI統合用)go test -json ./...

テスト設定

bash
# テストタイムアウトgo test -timeout 30s ./...
# 短時間テスト(長時間テストをスキップ)go test -short ./...
# ビルドキャッシュのクリアgo clean -testcachego test ./...

ベストプラクティス

DRY(Don't Repeat Yourself)原則

go
// Good: テーブル駆動テストで繰り返しを削減func TestValidation(t *testing.T) {    tests := []struct {        input string        valid bool    }{        {"[email protected]", true},        {"invalid-email", false},        {"", false},    }
    for _, tt := range tests {        t.Run(tt.input, func(t *testing.T) {            err := Validate(tt.input)            if (err == nil) != tt.valid {                t.Errorf("Validate(%q) error = %v, want valid = %v",                    tt.input, err, tt.valid)            }        })    }}

テストデータの分離

go
// Good: テストデータを testdata/ ディレクトリに配置func TestLoadConfig(t *testing.T) {    data, err := os.ReadFile("testdata/config.json")    if err != nil {        t.Fatal(err)    }
    config, err := ParseConfig(data)    // ...}

クリーンアップの使用

go
func TestWithCleanup(t *testing.T) {    // リソースを設定    file, err := os.CreateTemp("", "test")    if err != nil {        t.Fatal(err)    }
    // クリーンアップを登録(deferに似ているが、サブテストで動作)    t.Cleanup(func() {        os.Remove(file.Name())    })
    // テストを続ける...}

エラーメッセージの明確化

go
// Bad: 不明確なエラーif result != expected {    t.Error("wrong result")}
// Good: コンテキスト付きエラーif result != expected {    t.Errorf("Calculate(%d) = %d; want %d", input, result, expected)}
// Better: ヘルパー関数の使用assertEqual(t, result, expected, "Calculate(%d)", input)

避けるべきアンチパターン

go
// Bad: 外部状態に依存func TestBadDependency(t *testing.T) {    result := GetUserFromDatabase("123") // 実際のDBを使用    // テストが壊れやすく遅い}
// Good: 依存を注入func TestGoodDependency(t *testing.T) {    mockDB := &MockDatabase{        users: map[string]User{"123": {ID: "123"}},    }    result := GetUser(mockDB, "123")}
// Bad: テスト間で状態を共有var sharedCounter int
func TestShared1(t *testing.T) {    sharedCounter++    // テストの順序に依存}
// Good: 各テストを独立させるfunc TestIndependent(t *testing.T) {    counter := 0    counter++    // 他のテストに影響しない}
// Bad: エラーを無視func TestIgnoreError(t *testing.T) {    result, _ := Process()    if result != expected {        t.Error("wrong result")    }}
// Good: エラーをチェックfunc TestCheckError(t *testing.T) {    result, err := Process()    if err != nil {        t.Fatalf("Process() error = %v", err)    }    if result != expected {        t.Errorf("got %v, want %v", result, expected)    }}

クイックリファレンス

コマンド/パターン目的
go test ./...すべてのテストを実行
go test -v詳細出力
go test -coverカバレッジレポート
go test -raceレースコンディション検出
go test -bench=.ベンチマークを実行
t.Run()サブテスト
t.Helper()テストヘルパー関数
t.Parallel()テストを並列実行
t.Cleanup()クリーンアップを登録
testdata/テストフィクスチャ用ディレクトリ
-short長時間テストをスキップ
-tags=integrationビルドタグでテストを実行

覚えておいてください: 良いテストは高速で、信頼性があり、保守可能で、明確です。複雑さより明確さを目指してください。

來源與署名

來源:affaan-m/ECC位於docs/ja-JP/skills/golang-testing提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架