Golang Testing

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

by affaan-mef648e01899ba3e8dc6371642deaaf64b4477775No license275K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 4 days ago

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

Instructions onlySoftware Development
AI-generated overview

Comprehensive Go testing strategy covering TDD, table-driven tests, mocking, benchmarks, fuzzing and coverage.

What it does
This skill provides guidance and code patterns for testing Go code, following a test-driven development workflow. It covers table-driven tests, subtests, assertions and helpers, interface-based mocking, HTTP handler and database testing, benchmarks, fuzzing, coverage, integration tests and parallel tests. It also lists Go test commands and common anti-patterns to avoid.
When to use it
Use it when writing new Go code, reviewing Go code, improving existing tests, raising test coverage, or debugging and fixing bugs in Go projects.
Requirements
No scripts are shipped; it is instructions only. Applying the examples assumes a Go toolchain and, for some patterns, external packages such as testcontainers-go and a database or container runtime.

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ビルドタグでテストを実行

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

Source and attribution

Source:affaan-m/ECCindocs/ja-JP/skills/golang-testingat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal