はじめに
Go言語でユニットテストを書く際、モックやテストスイートをどのように管理していますか?この記事では、テストフレームワーク Testify を使って効率的かつわかりやすいテストコードを書く方法を紹介します。Testifyを選んだ理由や具体的な使い方を丁寧に解説し、初心者から上級者まで納得できるテスト実装方法を共有します。
Testifyを選んだ理由
1. 直感的なモック機能
Testifyは、簡単にモック(Mock)を作成・利用できるライブラリです。
例えば、データベースや外部APIといった実環境の依存をモックに置き換えることで、ユースケースのテストを柔軟に行えます。
2. テストスイートの提供
Testifyはsuite
パッケージを使い、テストケースのセットアップや共通処理をまとめることができます。これにより、コードの重複を減らし、テストコードをシンプルでメンテナンスしやすくします。
3. 多彩なアサーション
TestifyはAssert
やRequire
など、多彩なアサーションを提供します。これにより、エラーや結果を簡潔かつ明確に検証できます。
今回テストする対象コード
今回は、記事データを取得するユースケース articleUseCase
をテスト対象にします。
articleUseCase
は以下のように実装されています。
package usecase
import (
"context"
"github.com/jijimama/newspaper-app/adapter/gateway"
"github.com/jijimama/newspaper-app/domain"
)
// ArticleUseCase はデータアクセスのためのインターフェース。
// データベースや外部APIから情報を取得するための方法を定義。
type ArticleUseCase interface {
// 記事一覧を取得するメソッド
GetArticles(ctx context.Context) ([]*domain.Article, error)
}
// articleUseCase は記事関連のビジネスロジックを管理する構造体。
// データを取得するためにリポジトリを使用。
type articleUseCase struct {
articleRepository gateway.ArticleRepository // 記事データを取得するためのリポジトリ
}
// NewArticleUseCase は articleUseCase を作成するファクトリ関数。
// リポジトリを引数として渡します。
// ユースケースがリポジトリ(データ操作)を利用できるようにするため
func NewArticleUseCase(articleRepository gateway.ArticleRepository) *articleUseCase {
return &articleUseCase{
articleRepository: articleRepository,
}
}
// GetArticles はリポジトリを使って記事一覧を取得。
// コントローラーから呼び出され、実際にデータを取得する役割を担う。
func (uc *articleUseCase) GetArticles(ctx context.Context) ([]*domain.Article, error) {
// リポジトリにデータを取得するよう依頼
return uc.articleRepository.GetArticles(ctx)
}
このコードは、リポジトリを通じて記事データを取得するシンプルなユースケースを定義しています。
Testifyを使ったテストコード
以下は、Testifyを使ってArticleUsecase
のテストを実装したコードです。
1. モックの実装
ArticleRepository
を模擬するモックを作成します。
package usecase
import (
"context"
"testing"
"github.com/jijimama/newspaper-app/domain"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
// mockArticleRepository は ArticleRepository インターフェースを模擬したモック構造体。
type mockArticleRepository struct {
mock.Mock
}
// NewMockArticleRepository は mockArticleRepository のインスタンスを生成するヘルパー関数。
func NewMockArticleRepository() *mockArticleRepository {
return &mockArticleRepository{}
}
// GetArticles はモックの GetArticles 実装。
// 引数と戻り値を記録し、期待される値を返す。
func (m *mockArticleRepository) GetArticles(ctx context.Context) ([]*domain.Article, error) {
args := m.Called(ctx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*domain.Article), args.Error(1)
}
解説
Testifyのmock.Mock
は、メソッド呼び出しの記録と挙動を制御するための基本的な仕組みを提供します。
type mockArticleRepository struct {
mock.Mock
}
この構造体は、ArticleRepository
インターフェースを模倣します。mock.Mock
を埋め込むことで、モックとしての機能(引数の記録、戻り値の制御)が使えるようになります。
func NewMockArticleRepository() *mockArticleRepository {
return &mockArticleRepository{}
}
NewMockArticleRepository
は、mockArticleRepository
のインスタンスを生成する関数です。このようなヘルパー関数を用意しておくことで、テストコード内で簡単にモックを作成できます。
func (m *mockArticleRepository) GetArticles(ctx context.Context) ([]*domain.Article, error) {
args := m.Called(ctx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*domain.Article), args.Error(1)
}
この部分がモックとしての核心です。
m.Called(ctx)
は、Testifyのmock.Mock
が提供するメソッドです。この呼び出しにより、次の処理が行われます:
-
記録:
GetArticles
が呼び出されたこと、そして渡された引数(ctx)が記録されます。 - 戻り値の取得: 事前に設定された戻り値がargsとして返されます。
args.Get(0)
は、GetArticles
の戻り値として設定されている第1引数を取得します。この戻り値はインターフェース型のinterface{}で返されるため、具体的な型に変換する必要があります。
以下のコードで型変換を行っています:
return args.Get(0).([]*domain.Article), args.Error(1)
ここでは、戻り値の1つ目(args.Get(0)
)を[]domain.Article
型に変換し、2つ目(args.Error(1)
)をエラーとして返しています。
2. テストスイートの定義
Testifyのsuite
パッケージを使い、テストスイートを定義します。
// ArticleUseCaseTestSuite は ArticleUseCase のテストスイートを定義。
type ArticleUseCaseTestSuite struct {
suite.Suite
articleUseCase *articleUseCase // テスト対象のユースケース
mockArticleRepo *mockArticleRepository // モックリポジトリ
}
// SetupTest は各テストケースの前に実行されるセットアップ処理。
func (suite *ArticleUseCaseTestSuite) SetupTest() {
suite.mockArticleRepo = NewMockArticleRepository() // モックリポジトリを初期化
suite.articleUseCase = NewArticleUseCase(suite.mockArticleRepo) // ユースケースにモックを注入
}
解説
ArticleUseCaseTestSuite
は、Testifyのsuite
パッケージを利用して作られたテストスイートです。このスイートを使用することで、複数のテストケースをグループ化し、それぞれのテストの実行前後に共通のセットアップや後処理を実行できます。
type ArticleUseCaseTestSuite struct {
suite.Suite
articleUseCase *articleUseCase // テスト対象のユースケース
mockArticleRepo *mockArticleRepository // モックリポジトリ
}
- suite.Suite: Testify が提供する基本的なテストスイートの機能を含む構造体で、これを埋め込むことで便利な機能(アサーションメソッドやテストのセットアップ/後処理)を使えるようになります。
- articleUseCase: テスト対象となるビジネスロジック(ユースケース)のインスタンスを保持します。
-
mockArticleRepo: テスト用に作成したモックリポジトリ(
mockArticleRepository
)のインスタンスを保持します。
func (suite *ArticleUseCaseTestSuite) SetupTest() {
suite.mockArticleRepo = NewMockArticleRepository() // モックリポジトリを初期化
suite.articleUseCase = NewArticleUseCase(suite.mockArticleRepo) // ユースケースにモックを注入
}
-
SetupTest メソッド:
-
suite.SetupTest()
は、各テストケースの実行前に必ず呼び出されるメソッドです。 - テストごとに必要な準備をこの中で実行することで、テスト間での状態の共有や副作用を防ぎます。
-
suite.mockArticleRepo = NewMockArticleRepository()
-
NewMockArticleRepository
を呼び出してモックリポジトリを作成します。 - 各テストが独立して動作するよう、毎回新しいモックインスタンスを生成します。
suite.articleUsecase = NewArticleUseCase(suite.mockArticleRepo)
- 初期化したモックリポジトリを引数として、テスト対象のユースケースを生成します。
- テスト中、ユースケースは実際のリポジトリではなく、このモックリポジトリを通じてデータにアクセスします。
3. テストケースの実装
成功ケース
GetArticles
が正常に記事データを取得する場合のテストを実装します。
// TestGetArticles は GetArticles が成功するテスト。
func (suite *ArticleUseCaseTestSuite) TestGetArticles() {
// テスト用のデータを準備
articles := []*domain.Article{
{
Year: 2023,
Month: 12,
Day: 1,
Content: "Article Content 1",
Newspaper: "Daily News",
ColumnName: "Opinion",
},
{
Year: 2023,
Month: 12,
Day: 2,
Content: "Article Content 2",
Newspaper: "Daily News",
ColumnName: "Sports",
},
}
// モックの挙動を設定
suite.mockArticleRepo.On("GetArticles", mock.Anything).Return(articles, nil)
// テスト対象の関数を実行
result, err := suite.articleUseCase.GetArticles(context.Background())
// 結果を検証
suite.Assert().NoError(err, "エラーが発生していないことを確認")
suite.Assert().Equal(len(articles), len(result), "取得した記事の件数が一致することを確認")
// 各フィールドの値を検証
for i, article := range articles {
suite.Assert().Equal(article.Year, result[i].Year, "記事の年が一致することを確認")
suite.Assert().Equal(article.Month, result[i].Month, "記事の月が一致することを確認")
suite.Assert().Equal(article.Day, result[i].Day, "記事の日が一致することを確認")
suite.Assert().Equal(article.Content, result[i].Content, "記事の内容が一致することを確認")
suite.Assert().Equal(article.Newspaper, result[i].Newspaper, "記事の新聞名が一致することを確認")
suite.Assert().Equal(article.ColumnName, result[i].ColumnName, "記事のコラム名が一致することを確認")
}
}
解説
suite.mockArticleRepo.On("GetArticles", mock.Anything).Return(articles, nil)
- mockのOn メソッドの役割:
-
mockArticleRepo
(モックリポジトリ)のGetArticles
メソッドが呼び出された際の挙動を設定します。 - 引数として、
GetArticles
メソッド名と、期待される引数を渡します。
-
result, err := suite.articleUseCase.GetArticles(context.Background())
-
ArticleUsecCase
のGetArticles
メソッドを実行します。 - 実行結果(
result
)とエラー(err
)を取得します。
suite.Assert().NoError(err, "エラーが発生していないことを確認")
suite.Assert().Equal(len(articles), len(result), "取得した記事の件数が一致することを確認")
// 各フィールドの値を検証
for i, article := range articles {
suite.Assert().Equal(article.Year, result[i].Year, "記事の年が一致することを確認")
suite.Assert().Equal(article.Month, result[i].Month, "記事の月が一致することを確認")
suite.Assert().Equal(article.Day, result[i].Day, "記事の日が一致することを確認")
suite.Assert().Equal(article.Content, result[i].Content, "記事の内容が一致することを確認")
suite.Assert().Equal(article.Newspaper, result[i].Newspaper, "記事の新聞名が一致することを確認")
suite.Assert().Equal(article.ColumnName, result[i].ColumnName, "記事のコラム名が一致することを確認")
}
-
Assert メソッドの役割:
- 実行結果が期待通りであることを確認します。
- Testifyが提供するアサーションメソッド(NoError, Equal など)を利用して、テストの成功条件を記述します。
4. スイートの実行
// テストスイートを実行するためのエントリーポイント。
func TestArticleUseCaseTestSuite(t *testing.T) {
suite.Run(t, new(ArticleUseCaseTestSuite)) // テストスイートを実行
}
解説
-
suite.Run:
- Testifyが提供する関数で、指定したテストスイートを実行します。
- 第一引数には
*testing.T
を渡し、go test
コマンドが期待する形式に適合させています。 - 第二引数には、新しく生成したスイート(
new(ArticleUseCaseTestSuite)
)を渡します。
5. 完成したコード
package usecase
import (
"context"
"testing"
"github.com/jijimama/newspaper-app/domain"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
// mockArticleRepository は ArticleRepository インターフェースを模擬したモック構造体。
type mockArticleRepository struct {
mock.Mock
}
// NewMockArticleRepository は mockArticleRepository のインスタンスを生成するヘルパー関数。
func NewMockArticleRepository() *mockArticleRepository {
return &mockArticleRepository{}
}
// GetArticles はモックの GetArticles 実装。
// 引数と戻り値を記録し、期待される値を返す。
func (m *mockArticleRepository) GetArticles(ctx context.Context) ([]*domain.Article, error) {
args := m.Called(ctx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*domain.Article), args.Error(1)
}
// ArticleUseCaseTestSuite は ArticleUseCase のテストスイートを定義。
type ArticleUseCaseTestSuite struct {
suite.Suite
articleUseCase *articleUseCase // テスト対象のユースケース
mockArticleRepo *mockArticleRepository // モックリポジトリ
}
// SetupTest は各テストケースの前に実行されるセットアップ処理。
func (suite *ArticleUseCaseTestSuite) SetupTest() {
suite.mockArticleRepo = NewMockArticleRepository() // モックリポジトリを初期化
suite.articleUseCase = NewArticleUseCase(suite.mockArticleRepo) // ユースケースにモックを注入
}
// TestGetArticles は GetArticles が成功するテスト。
func (suite *ArticleUseCaseTestSuite) TestGetArticles() {
// テスト用のデータを準備
articles := []*domain.Article{
{
Year: 2023,
Month: 12,
Day: 1,
Content: "Article Content 1",
Newspaper: "Daily News",
ColumnName: "Opinion",
},
{
Year: 2023,
Month: 12,
Day: 2,
Content: "Article Content 2",
Newspaper: "Daily News",
ColumnName: "Sports",
},
}
// モックの挙動を設定
suite.mockArticleRepo.On("GetArticles", mock.Anything).Return(articles, nil)
// テスト対象の関数を実行
result, err := suite.articleUseCase.GetArticles(context.Background())
// 結果を検証
suite.Assert().NoError(err, "エラーが発生していないことを確認")
suite.Assert().Equal(len(articles), len(result), "取得した記事の件数が一致することを確認")
// 各フィールドの値を検証
for i, article := range articles {
suite.Assert().Equal(article.Year, result[i].Year, "記事の年が一致することを確認")
suite.Assert().Equal(article.Month, result[i].Month, "記事の月が一致することを確認")
suite.Assert().Equal(article.Day, result[i].Day, "記事の日が一致することを確認")
suite.Assert().Equal(article.Content, result[i].Content, "記事の内容が一致することを確認")
suite.Assert().Equal(article.Newspaper, result[i].Newspaper, "記事の新聞名が一致することを確認")
suite.Assert().Equal(article.ColumnName, result[i].ColumnName, "記事のコラム名が一致することを確認")
}
}
// テストスイートを実行するためのエントリーポイント。
func TestArticleUseCaseTestSuite(t *testing.T) {
suite.Run(t, new(ArticleUseCaseTestSuite)) // テストスイートを実行
}
処理の流れ
-
go test ./...
が実行される。 -
./usecase/
ディレクトリ内の*_test.go
ファイルを探す。 -
article_test.go
ファイルを見つける。 -
article_test.go
内のTestArticleUsecaseTestSuite
関数を見つける。
・Test
で始まる関数を探します。これらの関数はtesting.T
型の引数を取ります。 -
TestArticleUsecaseTestSuite
関数を実行し、suite.Run(t, new(ArticleUseCaseTestSuite))
を呼び出す。 -
suite.Run
がArticleUseCaseTestSuite
内の各テストメソッド(TestGetArticles
)を実行する。
・SetupTest
は各テストメソッド (TestGetArticles
) が実行される前に呼ばれます。これにより、各テストメソッドが実行される前に必要な初期化処理を行うことができます。 - 各テストメソッドの結果を収集し、最終的なテスト結果を表示する。
まとめ
この記事では、Testifyを使ったGoのテスト実装について解説しました。以下がポイントです:
- Testifyはモックやテストスイートでテストを簡素化。
- リファクタリングしやすいテストコードを書くのに役立つ。
Testifyを活用して、効率的かつ明快なテストコードを書いてみてください!