概要
- 実務でよくありそうな現在時刻の取得処理をインターフェース化し、テストのしやすい構造にする
- 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)
}
}
}