🕰️

Go言語でオニオンアーキテクチャ:日時をモックしたい?インフラ層で実装しよう

に公開

Go言語でオニオンアーキテクチャ(Onion Architecture)を採用してアプリケーションを構築する際、時間関連の処理をどこに実装すべきかを考えてみましょう。日時のモックはテストでよく必要になる要素ですが、各レイヤーがどのように役立つかを具体的に見ていきます。

オニオンアーキテクチャとは

オニオンアーキテクチャは、ドメイン共通の要件を中心に整理し、レイヤーに分けて実装するソフトウェア設計の手法です。この設計により、変更のしやすさや保守性が向上し、特にテストとメンテナンスがしやすくなります。

レイヤー構成

  1. ドメイン層: ビジネスロジックやルールを定義します。

  2. ユースケース層: ビジネスロジックを具体的な操作として整理・実行します。

  3. プレゼンテーション層: ユーザー入力を受け取り、ユースケースを呼び出します。

  4. インフラ層: DBやAPIなどの外部サービスとの連携を実装します。

日時の処理をどのレイヤーで実装する?

それでは、日時の処理をどのレイヤーで実装するのが適切でしょうか?

「現在時刻の取得」は OS の time.Now() に依存するため、ドメイン知識ではない外部的なリソース と捉えられます。したがって、この機能を直接ドメイン層やアプリケーション層で呼び出す形にせず、インタフェースで抽象化し、実装をインフラ層に配置することで依存方向を守ることができます。

日時関連のモック実装

日時をモックするのは、特にテスト環境でとても便利です。例えば、時間依存の処理を再現性のある形でテストできるようになります。そのため、インフラ層に日時の処理を実装することで、プロダクション環境とテスト環境の切り替えを簡単にし、全体のコード構造を保ちながら柔軟に運用することができます。

インタフェースによる日時の抽象化

日時の処理を抽象化するには、次のようなインタフェースを使用します。

package timeprovider

type TimeProvider interface {
    Now() time.Time
}

実装例

プロダクション環境用

package timeprovider

import "time"

type RealTimeProvider struct {}

func (r RealTimeProvider) Now() time.Time {
    return time.Now()
}

テスト環境用(モック)

package timeprovider

import "time"

type MockTimeProvider struct {
    FixedTime time.Time
}

func (m MockTimeProvider) Now() time.Time {
    return m.FixedTime
}

使用例

以下は、TimeProviderインタフェースを使用したコード例です。

ユースケースでの利用

package usecase

import (
    "your_project/domain"
    "your_project/timeprovider"
)

type SomeUseCase struct {
    repo domain.SomeRepository
    tp   timeprovider.TimeProvider
}

func NewSomeUseCase(repo domain.SomeRepository, tp timeprovider.TimeProvider) *SomeUseCase {
    return &SomeUseCase{
        repo: repo,
        tp:   tp,
    }
}

func (uc *SomeUseCase) DoSomething() error {
    someEntity := &domain.SomeEntity{
        ID:        1,
        Name:      "Alice",
        CreatedAt: uc.tp.Now(),
    }
    return uc.repo.Save(someEntity)
}

テストコードでの利用

package usecase_test

import (
    "testing"
    "time"

    "your_project/domain"
    "your_project/timeprovider"
    "your_project/usecase"
)

func TestSomeUseCase_DoSomething(t *testing.T) {
    mockRepo := &domain.MockSomeRepository{}
    mockTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
    mockProvider := timeprovider.MockTimeProvider{FixedTime: mockTime}

    uc := usecase.NewSomeUseCase(mockRepo, mockProvider)

    err := uc.DoSomething()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if mockRepo.SavedEntity.CreatedAt != mockTime {
        t.Errorf("expected %v, got %v", mockTime, mockRepo.SavedEntity.CreatedAt)
    }
}

日時処理をインフラ層で実装することの利点

日時の処理をインフラ層に実装すると、以下のような利点があります。

  1. テスト性の向上: モックを活用することで、不確定な日時に依存せずに再現性の高いテストを実行できます。
  2. 環境に応じた切り替えが簡単: 実際の日時が必要なプロダクション環境ではRealTimeProviderを、固定日時が必要なテスト環境ではMockTimeProviderを使用することで、ユースケース層のコードを変更せずに環境に適応できます。
  3. 責務の明確化: 日時処理をインフラ層に集中させることで、他の層が日時処理に直接依存せず、シンプルな設計を維持できます。

まとめ

日時の処理をインフラ層に実装すると、設計がより明確になります。例えば、テスト環境では固定した日時を返す仕組みを使い、時間に依存するロジックを安定して検証できます。一方で、プロダクション環境ではリアルタイムの日時を利用できるため、環境に応じた切り替えが簡単に行えます。このように、コード全体の構造が整理され、保守性が向上します。

Discussion