技能 编程开发 Go 测试策略指南

Go 测试策略指南

v20260224
golang-testing
面向Go开发者,介绍TDD、表驱动测试、子测试、断言助手、接口/时间/HTTP模拟等策略,帮助构建高质量测试体系。
获取技能
455 次下载
概览

Go テスト

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

いつ有効化するか

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

核となる原則

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

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

// 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. テーブル駆動テスト

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

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. サブテスト

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

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: "test@example.com"}
            if err := user.Validate(); err != nil {
                t.Errorf("unexpected error: %v", err)
            }
        })
    })

    t.Run("serialization", func(t *testing.T) {
        // 別のテストグループ
    })
}

テスト構成

ファイル構成

mypackage/
├── user.go
├── user_test.go          # ユニットテスト
├── integration_test.go   # 統合テスト
├── testdata/             # テストフィクスチャ
│   ├── valid_user.json
│   └── invalid_user.json
└── export_test.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のみをテスト
}

アサーションとヘルパー

基本的なアサーション

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")
    }
}

カスタムヘルパー関数

// ヘルパーとしてマーク(スタックトレースに表示されない)
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")
}

ディープ等価性チェック

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)
}

モッキングとスタブ

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

// 本番コード
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}

    // サービスをテスト...
}

時間のモック

// プロダクションコード - 時間を注入可能にする
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 クライアントのモック

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 の使用

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"])
    }
}

ミドルウェアのテスト

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)
            }
        })
    }
}

テストサーバー

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"])
    }
}

データベーステスト

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

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)
        })
    }
}

テストフィクスチャ

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', 'test@example.com')`,
        `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)
        }
    }
}

ベンチマーク

基本的なベンチマーク

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"))
    }
}

サブベンチマーク

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 test -bench=. -benchmem
func 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+)

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")
        }
    })
}

より複雑なファジング

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)
            }
        }
    })
}

テストカバレッジ

カバレッジの実行と表示

# カバレッジを実行して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 ./...

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

// 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:build integration
// +build integration

package myapp_test

import "testing"

func TestDatabaseIntegration(t *testing.T) {
    // 実際のDBを必要とするテスト
}
# 統合テストを実行
go test -tags=integration ./...

# 統合テストを除外
go test ./...

テストコンテナの使用

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
}

テストの並列化

並列テスト

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)
        })
    }
}

並列実行の制御

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ツール統合

テストコマンド

# 基本テスト
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 ./...

テスト設定

# テストタイムアウト
go test -timeout 30s ./...

# 短時間テスト(長時間テストをスキップ)
go test -short ./...

# ビルドキャッシュのクリア
go clean -testcache
go test ./...

ベストプラクティス

DRY(Don't Repeat Yourself)原則

// Good: テーブル駆動テストで繰り返しを削減
func TestValidation(t *testing.T) {
    tests := []struct {
        input string
        valid bool
    }{
        {"valid@email.com", 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)
            }
        })
    }
}

テストデータの分離

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

    config, err := ParseConfig(data)
    // ...
}

クリーンアップの使用

func TestWithCleanup(t *testing.T) {
    // リソースを設定
    file, err := os.CreateTemp("", "test")
    if err != nil {
        t.Fatal(err)
    }

    // クリーンアップを登録(deferに似ているが、サブテストで動作)
    t.Cleanup(func() {
        os.Remove(file.Name())
    })

    // テストを続ける...
}

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

// 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)

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

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

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

信息
Category 编程开发
Name golang-testing
版本 v20260224
大小 21KB
更新时间 2026-02-26
语言