0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go言語で始めるモックとテスト!Testifyで効率的なユースケーステストを実現する方法

Posted at

はじめに

Go言語でユニットテストを書く際、モックやテストスイートをどのように管理していますか?この記事では、テストフレームワーク Testify を使って効率的かつわかりやすいテストコードを書く方法を紹介します。Testifyを選んだ理由や具体的な使い方を丁寧に解説し、初心者から上級者まで納得できるテスト実装方法を共有します。

Testifyを選んだ理由

1. 直感的なモック機能

Testifyは、簡単にモック(Mock)を作成・利用できるライブラリです。
例えば、データベースや外部APIといった実環境の依存をモックに置き換えることで、ユースケースのテストを柔軟に行えます。

2. テストスイートの提供

Testifyはsuiteパッケージを使い、テストケースのセットアップや共通処理をまとめることができます。これにより、コードの重複を減らし、テストコードをシンプルでメンテナンスしやすくします。

3. 多彩なアサーション

TestifyはAssertRequireなど、多彩なアサーションを提供します。これにより、エラーや結果を簡潔かつ明確に検証できます。

今回テストする対象コード

今回は、記事データを取得するユースケース articleUseCase をテスト対象にします。
articleUseCase は以下のように実装されています。

backend/usecase/article.go
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 を模擬するモックを作成します。

backend/usecase/article_test.go
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パッケージを使い、テストスイートを定義します。

backend/usecase/article_test.go
// 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 が正常に記事データを取得する場合のテストを実装します。

backend/usecase/article_test.go
// 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())
  • ArticleUsecCaseGetArticles メソッドを実行します。
  • 実行結果(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. スイートの実行

backend/usecase/article_test.go
// テストスイートを実行するためのエントリーポイント。
func TestArticleUseCaseTestSuite(t *testing.T) {
    suite.Run(t, new(ArticleUseCaseTestSuite)) // テストスイートを実行
}

解説

  • suite.Run:
    • Testifyが提供する関数で、指定したテストスイートを実行します。
    • 第一引数には*testing.Tを渡し、go testコマンドが期待する形式に適合させています。
    • 第二引数には、新しく生成したスイート(new(ArticleUseCaseTestSuite))を渡します。

5. 完成したコード

backend/usecase/article_test.go
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)) // テストスイートを実行
}

処理の流れ

  1. go test ./... が実行される。
  2. ./usecase/ ディレクトリ内の *_test.go ファイルを探す。
  3. article_test.go ファイルを見つける。
  4. article_test.go 内の TestArticleUsecaseTestSuite 関数を見つける。
    Test で始まる関数を探します。これらの関数は testing.T 型の引数を取ります。
  5. TestArticleUsecaseTestSuite 関数を実行し、suite.Run(t, new(ArticleUseCaseTestSuite)) を呼び出す。
  6. suite.RunArticleUseCaseTestSuite 内の各テストメソッド(TestGetArticles)を実行する。
    SetupTest は各テストメソッド (TestGetArticles ) が実行される前に呼ばれます。これにより、各テストメソッドが実行される前に必要な初期化処理を行うことができます。
  7. 各テストメソッドの結果を収集し、最終的なテスト結果を表示する。

まとめ

この記事では、Testifyを使ったGoのテスト実装について解説しました。以下がポイントです:

  • Testifyはモックやテストスイートでテストを簡素化。
  • リファクタリングしやすいテストコードを書くのに役立つ。

Testifyを活用して、効率的かつ明快なテストコードを書いてみてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?