現在の日時が絡む処理の単体テストを書いてみる@Golang
time.Now()を使うような、現在の日時が絡む処理の単体テストを書くのって大変だと思います。
1時間ごとに結果が変わるような処理の場合、1時間に1回動かしてテストするなんて現実的じゃないですし、テストサーバの時刻いじるのも嫌です。
ランダムとかもそうなんですが、実行するたびに違うものを使うとどうしてもテストが大変なんですよねー。
そういう大変なのを回避しながら、単体テストを書いてみようと思います。
まず実行毎に変わっちゃうのの影響をなくす
ぱっと思いつく解決方法は、毎回変わる値を外部から渡すようにするって方法です。
外部から渡すのの代表的な方法としては、引数で渡すって感じですかね。
でもこの方法だと、引数で渡す側で現在日時を取ったりするから、そっち側のテストが難しくなっちゃいます。これじゃあんまり意味がないですね。
ってことで、やっぱり外部注入です!
外部注入するための時計をつくる
clock.go
code:golang
// 時計のインターフェース
type ClockInterface interface {
Now() time.Time
}
// 普段使う用の時計
type Clock struct {}
func (c *Clock) Now() time.Time {
return time.Now()
}
// テストで使う用の時計
type ClockAtTest struct {
CurrentTime time.Time
}
func (c *ClockAtTest) Now() time.Time {
return c.CurrentTime
}
Now()という関数を実装に持つClockInterfaceを定義します。
そして普段使うためのClock構造体と、テストでつかうためのClockAtTest構造体を用意します。
Clock構造体はtime.Now()を返すだけのものです。ClockAtTest構造体は、フィールドのCurrentTimeを返します。
テストではClockAtTestを使うことで、Nowを叩いてもあらかじめ指定した時間が返ります。
これでいつでも任意の時間のテストができるようになるはずです!!
使う側はInterfaceを持つようにする
Clock構造体をそのままもらってきて使ってしまうと、結局time.Nowが叩かれるだけになってしまいます。
ClockAtTest構造体も持てるよう、ClockInterfaceをもらうようにしておきます。
今回は外部注入って名前も使ってるんで、構造体のフィールドとしてClockInterfaceを取るようにします。
code:golang
type Timer struct {
clock ClockInterface
}
func (t *Timer) Message() string {
switch t.clock.Now().Hour() {
case 9, 10, 11, 13, 14, 15, 16, 17:
return "働け"
case 12:
return "昼飯くうでー"
case 18:
return "帰んでー"
default:
return "暇やでー"
}
}
こんな感じのTimer構造体を定義します。
労働時間なら働けって言うて、それ以外はなんか自由にしていいでって感じのメッセージを返すだけの構造体です。
Timerを使うときはclockにClockInterfaceを実装した構造体を入れるようにします。
code:golang
timer := &Timer{
clock: new(Clock),
}
テストならClockAtTest
code:golang
time := &Timer{
clock: new(ClockAtTest),
}
あとはテストを書くだけ
code:golang
func TestTimer_Message(t *testing.T) {
timer := &Timer{
clock: new(ClockAtTest),
}
testTable := []struct{
current time.Time
expect string
}{
{time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 1, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 2, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 3, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 4, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 5, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 6, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 7, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 8, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 9, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 10, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 11, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 12, 0, 0, 0, time.Local), "昼飯くうでー"},
{time.Date(2020, 1, 1, 13, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 14, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 15, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 16, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 17, 0, 0, 0, time.Local), "働け"},
{time.Date(2020, 1, 1, 18, 0, 0, 0, time.Local), "帰んでー"},
{time.Date(2020, 1, 1, 19, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 20, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 21, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 22, 0, 0, 0, time.Local), "暇やでー"},
{time.Date(2020, 1, 1, 23, 0, 0, 0, time.Local), "暇やでー"},
}
for i, test := range testTable {
timer.clock = &ClockAtTest{CurrentTime: test.current}
actual := timer.Message()
if test.expect != actual {
t.Fatalf("%s No.%02d 失敗\n期待: %s\n実際: %s\n", t.Name(), i+1, test.expect, actual)
}
}
}
おわりに
単体テストを書こうとすると、どうしても依存と戦うことになりますね。
その回答として比較的簡単に実装できるのがinterfaceを使ったものだと思います。
ダックタイピングになれるところから始まりますし、interfaceを剥いて構造体を取り出すなんてのも必要になることもあり、
そのまま構造体を渡したり、依存しまくったりするより難易度が高いですが、恩恵も大きいと思います。
最終更新日 : 2019/06/24