🏑

Go言語でオニオンアーキテクチャ:モック作成に関数フィールドを使おう

2025/01/04に公開

オニオンアーキテクチャは、アプリケーションを層(レイヤー)に分けて依存関係を整理する設計手法です。特に、ビジネスロジックを中心に据え、その周辺に外部との接続層を配置することで、テスト可能性や保守性を向上させる利点があります。本記事では、Go言語でこのアーキテクチャを採用する際に、リポジトリをモックする方法として「関数フィールド」を活用するアイデアを紹介します。


オニオンアーキテクチャの基本構造

オニオンアーキテクチャの構造は以下の通りです:

  1. ドメイン層: ビジネスロジックやエンティティを含む。
  2. ユースケース層: アプリケーションのユースケースを実現するロジック。
  3. インフラ層: データベースやAPIなど外部リソースとの接続を扱う。

このアーキテクチャでは、インフラ層がドメイン層に依存しない設計が推奨されます。これにより、ドメインロジックを中心とした変更が容易になり、テスト性も向上します。


リポジトリのモック化と関数フィールドの活用

テスト時には、インフラ層に実際のデータベース接続を使用するのではなく、リポジトリのモックを使用します。以下のようなシナリオが考えられます:

  • 正常に処理した場合
  • 不正なパラメータによるエラー発生時
  • データベース接続エラーが発生した場合

Go言語では、構造体に関数フィールドを持たせることで、これらのシナリオに対応したモックを簡単に作成できます。

サンプルコード

以下に、リポジトリのモックを関数フィールドで実現する方法を示します。

// ドメインのエンティティ
package domain

type User struct {
    ID   int
    Name string
}
// リポジトリインターフェース
package domain

import "example.com/project/domain"

type UserRepository interface {
    GetUserByID(id int) (*domain.User, error)
    SaveUser(user *domain.User) error
}
// モックリポジトリ
package infra

import (
    "example.com/project/domain"
)

type MockUserRepository struct {
    GetUserByIDFunc func(id int) (*domain.User, error)
    SaveUserFunc    func(user *domain.User) error
}

func (m *MockUserRepository) GetUserByID(id int) (*domain.User, error) {
    return m.GetUserByIDFunc(id)
}

func (m *MockUserRepository) SaveUser(user *domain.User) error {
    return m.SaveUserFunc(user)
}

テストコード例

以下に、関数フィールドを活用したテストコードの例を示します。

package usecase

import (
    "errors"
    "testing"

    "example.com/project/domain"
    "example.com/project/usecase"
)

func TestGetUserByID(t *testing.T) {
    t.Run("指定したIDのユーザーが存在している場合", func(t *testing.T) {
        mockRepo := &infra.MockUserRepository{
            GetUserByIDFunc: func(id int) (*domain.User, error) {
                return &domain.User{ID: 1, Name: "Alice"}, nil
            },
        }
        uc := usecase.NewUserUseCase(mockRepo)

        user, err := uc.GetUser(1)
        if err != nil {
            t.Errorf("unexpected error: %v", err)
        }
        if user.Name != "Alice" {
            t.Errorf("expected user name to be Alice, got %s", user.Name)
        }
    })

    t.Run("指定したIDのユーザーが存在しない場合", func(t *testing.T) {
        mockRepo := &infra.MockUserRepository{
            GetUserByIDFunc: func(id int) (*domain.User, error) {
                return nil, errors.New("user not found")
            },
        }
        uc := usecase.NewUserUseCase(mockRepo)

        _, err := uc.GetUser(1)
        if err == nil {
            t.Error("expected error, got nil")
        }
        if err.Error() != "user not found" {
            t.Errorf("expected error message to be 'user not found', got %s", err.Error())
        }
    })

    t.Run("予期せぬエラーが発生した場合", func(t *testing.T) {
        mockRepo := &infra.MockUserRepository{
            GetUserByIDFunc: func(id int) (*domain.User, error) {
                return nil, errors.New("something went wrong")
            },
        }
        uc := usecase.NewUserUseCase(mockRepo)

        _, err := uc.GetUser(1)
        if err == nil {
            t.Error("expected error, got nil")
        }
        if err.Error() != "something went wrong" {
            t.Errorf("expected error message to be 'something went wrong', got %s", err.Error())
        }
    })
}

関数フィールドを使う利点

  1. 簡潔さ: 必要な部分のみをモック化できる。
  2. 柔軟性: 複数のテストケースで異なる動作を設定可能。
  3. テストの明確化: テストケースごとにモックの挙動を個別に定義することで、テスト対象の動作が分かりやすくなる。

まとめ

Go言語でオニオンアーキテクチャを採用する際、リポジトリのモックを関数フィールドで実装することで、テスト効率を劇的に向上させることができます。柔軟で簡潔なモックを活用して、より堅牢で保守性の高いアプリケーションを目指しましょう。

Discussion