📚

【Go】現在時刻に関する処理をインターフェース化してテストしやすくするサンプルコード(DI, 依存性注入)

に公開

概要

  • 実務でよくありそうな現在時刻の取得処理をインターフェース化し、テストのしやすい構造にする
    • DIを使用してテストしやすいコードのサンプル
    • コンストラクタインジェクションを利用

サンプルコード

  • 現在時刻を生成する箇所をインターフェース化することで、テスト時にモックに差し替える事ができる
    • テスト実行時の現在時刻に依存することなくテストが可能になる
package main

import (
	"fmt"
	"time"
)

// 現在時刻を提供するインターフェース
type TimeProvider interface {
	Now() time.Time
}

type RealTimeProvider struct{}

func (tp *RealTimeProvider) Now() time.Time {
	return time.Now()
}

// ExpiryCheckerは期限切れをチェックする構造体
type ExpiryChecker struct {
	TimeProvider TimeProvider
}

func NewExpiryChecker(tp TimeProvider) *ExpiryChecker {
	return &ExpiryChecker{TimeProvider: tp}
}

func (ec *ExpiryChecker) IsExpired(deadline time.Time) bool {
	// time.Now()を直接呼び出すとテストの実行時刻に依存してしまう
	// そのため、TimeProviderインターフェースを通じて現在時刻を取得する
	// これにより、テスト時にモックを使って現在時刻を制御できる
	return ec.TimeProvider.Now().After(deadline)
}

func main() {
	checker := NewExpiryChecker(&RealTimeProvider{})

	// DBなどに保存されてる「期限」など
	deadline := time.Date(2025, 5, 9, 12, 0, 0, 0, time.Local)

	if checker.IsExpired(deadline) {
		fmt.Println("期限切れです")
	} else {
		fmt.Println("まだ有効です")
	}
}

テストコード

package main

import (
	"testing"
	"time"
)

// 現在時刻を提供するインターフェースのモック
type MockTimeProvider struct {
	FixedTime time.Time
}

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

func TestIsExpired(t *testing.T) {
	deadline := time.Date(2025, 5, 9, 12, 0, 0, 0, time.Local)

	tests := []struct {
		name     string
		now      time.Time
		expected bool
	}{
		{"期限前", time.Date(2025, 5, 9, 11, 0, 0, 0, time.Local), false},
		{"期限後", time.Date(2025, 5, 9, 13, 0, 0, 0, time.Local), true},
	}

	for _, tt := range tests {
		// モックを使って現在時刻を制御
		// これにより、テストの実行時刻に依存しない
		mock := &MockTimeProvider{FixedTime: tt.now}
		checker := NewExpiryChecker(mock)

		got := checker.IsExpired(deadline)
		if got != tt.expected {
			t.Errorf("[%s] expected %v, got %v", tt.name, tt.expected, got)
		}
	}
}
GitHubで編集を提案

Discussion