【Go言語】サービス層の実装とテスト環境構築のベストプラクティス
こんにちは!フリーランスエンジニアのこたろうです。
Goでのバックエンド開発において、適切なサービス層の実装とテスト環境の構築は非常に重要です。今回は、t.Cleanup
を活用したテストデータの管理方法と、実践的なサービス層の実装について解説します。
サービス層の実装とは?
サービス層は、ビジネスロジックを担う重要な層です。以下のような特徴があります:
- ビジネスロジックの集約
- データベース操作の抽象化
- テスタビリティの確保
基本的な実装例
1. サービス構造体の定義
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から導入された機能です。
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. テストデータの管理
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
}
テスト実装のポイント
-
t.Cleanupの活用
- テスト後の後処理を簡潔に記述
- 複数の後処理を順番に登録可能
- deferよりも柔軟な使用が可能
-
テストデータの管理
- テストデータの準備と削除を明確に分離
- トランザクションを活用したロールバック
- テストケースごとに独立した環境を維持
-
テーブルドリブンテストの活用
- 複数のテストケースを効率的に管理
- エッジケースの網羅的なテスト
- テストケースの追加が容易
まとめ
サービス層のテスト実装において重要なポイント:
-
t.Cleanup
を活用した効率的な後処理 - テストデータの適切な準備と削除
- インターフェースを活用した依存性の注入
- テーブルドリブンテストによる網羅的なテスト
これらの方法を組み合わせることで、保守性が高く、信頼性のあるテストを実装することができます。
参考文献
- 『Goで作るはじめてのWebアプリケーション 改訂版』技術書典