【Go言語】サービス層のテスト実装とt.Cleanupを使った効率的なテストデータ管理

# 【Go言語】サービス層の実装とテスト環境構築のベストプラクティス

こんにちは！フリーランスエンジニアのこたろうです。

Goでのバックエンド開発において、適切なサービス層の実装とテスト環境の構築は非常に重要です。今回は、`t.Cleanup`を活用したテストデータの管理方法と、実践的なサービス層の実装について解説します。

## サービス層の実装とは？

サービス層は、ビジネスロジックを担う重要な層です。以下のような特徴があります：

- ビジネスロジックの集約
- データベース操作の抽象化
- テスタビリティの確保

## 基本的な実装例

### 1. サービス構造体の定義

```go
package services

type ArticleService struct {
    DB Database // インターフェースを使用して依存性を注入
}

// Databaseインターフェース
type Database interface {
    SelectArticleDetail(id int) (Article, error)
    Close() error
}

// 記事取得のメソッド
func (s *ArticleService) GetArticleDetail(articleID int) (Article, error) {
    article, err := s.DB.SelectArticleDetail(articleID)
    if err != nil {
        return Article{}, fmt.Errorf("failed to get article: %w", err)
    }
    return article, nil
}
```

## テスト環境の構築

### 1. t.Cleanupの活用

`t.Cleanup`は、テスト後の後処理を簡潔に書けるGo 1.14から導入された機能です。

```go
func TestGetArticleDetail(t *testing.T) {
    // テストDB設定
    db := setupTestDB(t)
    // テスト終了時にDBをクローズ
    t.Cleanup(func() {
        if err := db.Close(); err != nil {
            t.Errorf("failed to close db: %v", err)
        }
    })

    // サービスの初期化
    svc := &services.ArticleService{DB: db}

    // テストケース実行
    tests := []struct {
        name     string
        articleID int
        want     Article
        wantErr  bool
    }{
        {
            name:     "正常系: 記事が見つかる",
            articleID: 1,
            want:     Article{ID: 1, Title: "テスト記事"},
            wantErr:  false,
        },
        {
            name:     "異常系: 記事が見つからない",
            articleID: 999,
            want:     Article{},
            wantErr:  true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := svc.GetArticleDetail(tt.articleID)
            if (err != nil) != tt.wantErr {
                t.Errorf("GetArticleDetail() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("GetArticleDetail() = %v, want %v", got, tt.want)
            }
        })
    }
}
```

### 2. テストデータの管理

```go
func setupTestDB(t *testing.T) *sql.DB {
    db, err := sql.Open("postgres", "postgres://user:pass@localhost:5432/testdb?sslmode=disable")
    if err != nil {
        t.Fatalf("failed to connect to test database: %v", err)
    }

    // テストデータの準備
    if err := prepareTestData(db); err != nil {
        t.Fatalf("failed to prepare test data: %v", err)
    }

    // テスト終了時にテストデータを削除
    t.Cleanup(func() {
        if err := cleanupTestData(db); err != nil {
            t.Errorf("failed to cleanup test data: %v", err)
        }
    })

    return db
}

func prepareTestData(db *sql.DB) error {
    queries := []string{
        `INSERT INTO articles (id, title, content) 
         VALUES (1, 'テスト記事', 'これはテスト用の記事です')`,
        `INSERT INTO articles (id, title, content) 
         VALUES (2, '別のテスト記事', 'これは別のテスト用記事です')`,
    }

    for _, q := range queries {
        if _, err := db.Exec(q); err != nil {
            return fmt.Errorf("failed to execute query: %w", err)
        }
    }
    return nil
}

func cleanupTestData(db *sql.DB) error {
    _, err := db.Exec("DELETE FROM articles WHERE id IN (1, 2)")
    return err
}
```

## テスト実装のポイント

1. **t.Cleanupの活用**
   - テスト後の後処理を簡潔に記述
   - 複数の後処理を順番に登録可能
   - deferよりも柔軟な使用が可能

2. **テストデータの管理**
   - テストデータの準備と削除を明確に分離
   - トランザクションを活用したロールバック
   - テストケースごとに独立した環境を維持

3. **テーブルドリブンテストの活用**
   - 複数のテストケースを効率的に管理
   - エッジケースの網羅的なテスト
   - テストケースの追加が容易

## まとめ

サービス層のテスト実装において重要なポイント：

- `t.Cleanup`を活用した効率的な後処理
- テストデータの適切な準備と削除
- インターフェースを活用した依存性の注入
- テーブルドリブンテストによる網羅的なテスト

これらの方法を組み合わせることで、保守性が高く、信頼性のあるテストを実装することができます。

## 参考文献

- 『Goで作るはじめてのWebアプリケーション 改訂版』技術書典
  - [https://techbookfest.org/product/jXDAEU1dR53kbZkgtDm9zx](https://techbookfest.org/product/jXDAEU1dR53kbZkgtDm9zx)