CtrlK
BlogDocsLog inGet started
Tessl Logo

golang-testing

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

45

Quality

32%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./docs/ja-JP/skills/golang-testing/SKILL.md
SKILL.md
Quality
Evals
Security

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

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

Repository
affaan-m/everything-claude-code
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.