- 投稿日:2020-04-03T16:14:07+09:00
[Golang]テスタブルな現在時刻取得処理を作る
はじめに
backendサービスを作っていると大抵どこかしらで現在時刻
time.Now()
を取り扱うことになると思います。
しかしtime.Now()
の値は、刻一刻と変わる値なのでtime.Now()
を使用する関数のtestを書く際には必ず
time.Now()
の値を固定化する手段を必ず作っておかなければなりません。Perlの場合は
Test::MockTime
、Rubyの場合はtimecop
、Pythonの場合はFreezeGun
のようなlibraryを使ってmockしますが
Goの場合このtime.Now()
を固定化するためにどうすればよいのか幾つか方法を試したので紹介します。monkey.Patch()
まずは monkey.Patch()を使う方法。
注意:一応書いておきますが、おすすめの方法ではありません。
monkey.Patch()は実行時に実行ファイルを書き換えて、mockしたい関数を呼び出すイリーガルなツールです。
既に実装されているプロダクトコードにテストを書かなきゃいけないなど
どうしても困った時に使うという感覚で使用してください。
まずは、適当に
time.Now()
を使うコードを書いてみます。package main import ( "context" "time" ) type Hoge struct { Now time.Time } func GetHoge(ctx context.Context) *Hoge { now := time.Now().UTC() return &Hoge{ Now: now, } }model用のstructに現在時刻を突っ込んで後々の処理に使うよくある実装ですね。
これのテストをmonkey.Patch()
で書いてみます。package main import ( "context" "testing" "time" "bou.ke/monkey" "github.com/stretchr/testify/assert" ) func TestGetHoge(t *testing.T) { t.Run("time mock test use monkey", func(t *testing.T) { ctx := context.Background() mockTime := time.Date(2020, 4, 3, 1, 2, 3, 123456000, time.UTC) patch := monkey.Patch(time.Now, func() time.Time { return mockTime }) defer patch.Unpatch() expected := &Hoge{Now: mockTime} actual := GetHoge(ctx) assert.Equal(t, expected, actual) }) }紹介する方法の中で使い方は一番簡単。
patch := monkey.Patch(time.Now, func() time.Time { return mockTime }) defer patch.Unpatch()この2行を書くだけで……あら不思議!
プロダクトコードに書かれたtime.Now()
の戻り値をmockした関数の値に置き換えてくれます。
プロダクトコードを全然変更していないのに関数の挙動が変化する文字通りモンキーパッチです。
defer patch.Unpatch()
を忘れると当該のテスト通過した後もpatchされたままになるので注意してください。ちなみに、沢山
monkey.Patch()
を仕込みたいときは一つずつdefer patch.Unpatch()
しなくてもdefer monkey.UnpatchAll()
で全てunpatch出来ます。monkey.Patch(fuga.GetFuga, func() *fuga.Fuga { return &fuga.Fuga{} }) monkey.Patch(Piyo.GetPiyo, func() *piyo.Piyo { return &piyo.Piyo{} }) monkey.Patch(time.Now, func() time.Time { return mockTime }) defer monkey.UnpatchAll()flextime.Now()
お次はflextimeを使う方法。
どういう発想のものなのか作者のブログ見るのが良いです。
https://songmu.jp/riji/entry/2020-01-19-flextime.htmlflextimeはテストコードの中で現在時刻を切り替えるためのライブラリです。Sleep時に実際に時間を止めずに時間が経過したように見せかける機能もあります。
つまり、PerlのTest::MockTimeやRubyのtimecop的なことをしたいわけですが、Goだとグローバルに関数の挙動を切り替えるといったことはできないため、利用にあたってはtimeパッケージで使っている関数を、flextimeパッケージに切り替える必要があります。
要するに
time.Now()
を使わずに、mock出来る独自のflextime.Now()
を使うという方法。
見て分かる通りプロダクトコードの書き換えが必須になります。こちらも紹介はしますが使うかどうかは自己責任でお願いします。
何故ならグローバルで定義されたmock出来る
flextime
を使う場合は
flextime.Now()
を使っている関数Aとは別の関数Bでダミーの値をSetするような処理を作り
その関数Bでflextime
をRestoreするのを忘れるバグがあった場合
関数Bとはまるで関係ない関数Aで取得した時刻が意図せずダミーの時刻になってしまう可能性があるからです。
先ほどのコードを
flextime.Now()
を使うように書き換えます。package main import ( "context" "time" "github.com/Songmu/flextime" ) type Hoge struct { Now time.Time } func GetHoge(ctx context.Context) *Hoge { now := flextime.Now().UTC() return &Hoge{ Now: now, } }次にテストコード
flextime.Set()
を使用してflextimeの内部時刻をmockの時刻に置き換えます。package main import ( "context" "testing" "time" "github.com/Songmu/flextime" "github.com/stretchr/testify/assert" ) func TestGetHoge(t *testing.T) { t.Run("time mock test use flextime", func(t *testing.T) { ctx := context.Background() mockTime := time.Date(2020, 4, 3, 1, 2, 3, 123456000, time.UTC) restore := flextime.Set(mockTime) defer restore() expected := &Hoge{Now: mockTime} actual := GetHoge(ctx) assert.Equal(t, expected, actual) }) }現在時刻とmock時刻はflextime内で管理されているので
flextime.Set()
するだけでflextime.Now()
の時刻を固定化することが出来ます。先ほどの話なのですが
flextime.Set()
をプロダクトコード内で使用してdefer restore()
を忘れるバグがあると
全てのflextime.Now()
を使用する関数で必ず時刻が同じになってしまうという重大なバグを生む可能性があります。Context.Value に現在時刻を保持する
さっきからオススメしないオススメしないばかり言って結局何をオススメするんだという話ですが
個人的にはこの「Context.Valueに現在時刻を保持する」案をオススメします。要するに
time.Now()
を関数内で行うからmockに困るのであって
現在時刻を関数の引数で渡してしまえばいいじゃないという話。
time.Now()
を行う場所はhandlerを呼ぶ前のmiddlewareレイヤーに移して
middleware内でtime.Now()
の結果をContext.Value
に格納します。
そして現在時刻を使用したい時はContext.Value
から現在時刻を取得するためのutilを使用するようにします。
まずは現在時刻を
context.Value
に格納したり、context.Value
から現在時刻を取得するutilを用意します。package timeutil import ( "context" "time" ) const CtxFreezeTimeKey = "freeze_time_key" // context.Valueから現在時刻を取得する関数 func Now(ctx context.Context) time.Time { return ctx.Value(CtxFreezeTimeKey).(time.Time) } // context.Valueに現在時刻を格納する関数 func SetNow(ctx context.Context) context.Context { return context.WithValue(ctx, CtxFreezeTimeKey, time.Now()) } // テスト用にmockしたいtime.Timeをcontext.Valueに格納する関数 func MockNow(ctx context.Context, mockTime time.Time) context.Context { return context.WithValue(ctx, CtxFreezeTimeKey, mockTime) }middlewareの実装は場合によって異なるので詳しくは書きませんが
例えばgPRC Serverの場合はServer Chainに以下のようなInterceptorを挟みます。func UnaryServerInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) { ctx = timeutil.SetNow(ctx) return handler(ctx, req) } }次に現在時刻を実際に使用する部分
package main import ( "context" "time" "sample-timemock/timeutil" ) type Hoge struct { Now time.Time } func GetHoge(ctx context.Context) *Hoge { now := timeutil.Now(ctx).UTC() return &Hoge{ Now: now, } }
time.Now()
をtimeutil.Now(ctx)
に置き換えています。
timeutil.Now(ctx)
はflextime.Now()
とは異なり引数のcontext.Context
から値を取り出すだけなので
utilでtime.Now()
をセットする処理がバグってるとか以外であれば他の関数の影響を受けることはありません。
まあそもそもutilでtime.Now()
をセットする処理がバグってる場合はutilのtestがpassしないはずです。では上記のテストを書いていきます。
package main import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "sample-timemock/timeutil" ) func TestGetHoge(t *testing.T) { t.Run("time mock test use flextime", func(t *testing.T) { mockTime := time.Date(2020, 4, 3, 1, 2, 3, 123456000, time.UTC) mockCtx := timeutil.MockNow(context.Background(), mockTime) expected := &Hoge{Now: mockTime} actual := GetHoge(mockCtx) assert.Equal(t, expected, actual) }) }やってることはcontext.Valueにmockのtime.Timeを突っ込んで引数で渡しているだけで非常にシンプル。
検証してきた中ではモンキーパッチも使わずグローバルなtime領域も汚さない一番良さそうな実装だと思います。・・・ただし
現在時刻をmiddlewareでセットする実装の場合は、requestでhandlerが呼ばれた時点での現在時刻をセットするため
そのスコープ内の現在時刻が必ず同じになります。例えば、独自でログ出力処理を書いていたりする場合
[2020-04-03 16:10:23.134] CreateHoge() [2020-04-03 16:10:23.134] CreateFuga() [2020-04-03 16:10:23.134] CreatePiyo() [2020-04-03 16:10:23.134] GetHoge()みたいに全部同じ時刻になってしまうので、
time.Now()
全てをtimeutil.Now(ctx)
に置き換えるかどうかは考えた方が良いですね。
- 投稿日:2020-04-03T08:14:05+09:00
Golangで、デザインパターン「Visitor」を学ぶ
GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Visitor」を学ぶ"今回は、Pythonで実装した”Visitor”のサンプルアプリをGolangで実装し直してみました。
■ Visitorパターン(ビジター・パターン)
Visitorパターンは、オブジェクト指向プログラミング およびソフトウェア工学 において、 アルゴリズムをオブジェクトの構造から分離するためのデザインパターンである。分離による実用的な結果として、既存のオブジェクトに対する新たな操作を構造を変更せずに追加することができる。
基本的には Visitorパターンは一群のクラスに対して新たな仮想関数をクラス自体を変更せずに追加できるようにする。そのために、全ての仮想関数を適切に特化させた Visitor クラスを作成する。Visitorはインスタンスへの参照を入力として受け取り、ダブルディスパッチを用いて目的を達する。
Visitor は強力であるが、既存の仮想関数と比較して制限もある。各クラス内に小さなコールバックメソッドを追加する必要があり、各クラスのコールバックメソッドは新たなサブクラスで継承することができない。UML class and sequence diagram
UML class diagram
□ 備忘録
書籍「増補改訂版Java言語で学ぶデザインパターン入門」の引用ですが、腹落ちしました。
Visitorとは、「訪問者」という意味です。データ構造の中にたくさんの要素が格納されており、その各要素に対して何らかの「処理」をしていくとしましょう。このとき、その「処理」のコードはどこに書くべきでしょうか?普通に考えれば、データ構造を表しているクラスの中に書きますね。でも、もし、その「処理」が一種類とは限らなかったらどうでしょう。その場合、新しい処理が必要になるたびに、データ構造のクラスを修正しなければならなくなります。
Visitor
パターンでは、データ構造と処理を分離します。そして、データ構造の中をめぐり歩く主体である「訪問者」を表すクラスを用意し、そのクラスに処理をまかせます。すると、新しい処理を追加したいときには新しい「訪問者」を作ればよいことになります。そして、データ構造の方は、戸を叩いてくる「訪問者」を受け入れてあげればよいのです。■ "Visitor"のサンプルプログラム
実際に、Visitorパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。ちなみに、Qiita記事「Golangで、デザインパターン「Composite」を学ぶ」でのサンプルプログラムと挙動が同じになるので、実装を比較してみると
Visitor
パターンの理解がより深まります。
- ルートエントリの
ディレクトリ
に、サブディレクトリ
およびファイル
を追加してみる- ルートエントリの
ディレクトリ
に、ユーザエントリのディレクトリ
を追加して、さらに、サブディレクトリ
およびファイル
を追加してみる- 敢えて、
ファイル
に、ディレクトリ
を追加して、失敗することを確認する$ go run Main.go Making root entries /root (30000) /root/bin (30000) /root/bin/vi (10000) /root/bin/latex (20000) /root/tmp (0) /root/usr (0) Making user entries /root (31500) /root/bin (30000) /root/bin/vi (10000) /root/bin/latex (20000) /root/tmp (0) /root/usr (1500) /root/usr/yuki (300) /root/usr/yuki/diary.html (100) /root/usr/yuki/composite.py (200) /root/usr/hanako (300) /root/usr/hanako/memo.tex (300) /root/usr/tomura (900) /root/usr/tomura/game.doc (400) /root/usr/tomura/junk.mail (500) Occurring Exception... FileTreatmentException■ サンプルプログラムの詳細
Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Visitor
- ディレクトリ構成
. ├── Main.go └── visitor ├── element.go └── visitor.go(1) Visitor(訪問者)の役
Visitor
役は、データ構造の具体的な要素(ConcreteElement
役)ごとに、「xxxxを訪問した」というvisit(xxxx)メソッドを宣言します。visit(xxxx)はxxxxを処理するためのメソッドです。実際のコードはConcreteVisitor
役の側に書かれます。
サンプルプログラムでは、Visitor
インタフェースが、この役を努めます。visitor/visitor.gopackage visitor import "fmt" // Visitor is interface type Visitor interface { visit(directory Entry) }(2) ConcreteVisitor(具体的訪問者)の役
ConcreteVisitor
役は、Visitor
役のインタフェースを実装します。visitor(xxxx)という形のメソッドを実装し、個々のConcreteElement
役ごとの処理を記述します。
サンプルプログラムでは、ListVistor
構造体が、この役を努めます。visitor/visitor.go// ListVisitor is struct type ListVisitor struct { currentdir string } // NewListVistor func for initializing ListVisitor func NewListVistor() *ListVisitor { return &ListVisitor{ currentdir: "", } } func (l *ListVisitor) visit(directory Entry) { fmt.Printf("%s/%s\n", l.currentdir, directory.toString()) if _, ok := directory.(*Directory); ok { savedir := l.currentdir l.currentdir = fmt.Sprintf("%s/%s", l.currentdir, directory.getName()) for _, f := range directory.getDir() { f.Accept(l) } l.currentdir = savedir } }(3) Element(要素)の役
Element
役は、Visitor
役の訪問先を表す役です。訪問先を受け入れるaccept
メソッドを宣言します。accept
メソッドの引数にはVisitor
役が渡されます。
サンプルプログラムでは、Entry
インタフェースが、この役を努めます。visitor/element.gopackage visitor import "fmt" // Entry is interface type Entry interface { getName() string getSize() int Accept(v Visitor) toString() string getDir() []Entry }(4) ConcreteElement(具体的要素)の役
ConcreteElement
役は、Element
役のインタフェースを実装する役です。
サンプルプログラムでは、File
構造体とDirectory
構造体が、この役を努めます。visitor/element.go// File is struct type File struct { Entry name string size int } // NewFile func for initializing File func NewFile(name string, size int) *File { return &File{ name: name, size: size, } } func (f *File) getName() string { return f.name } func (f *File) getSize() int { return f.size } // Add func for adding file func (f *File) Add(entry Entry) { if err := doError(); err != nil { fmt.Println(err) } } // Accept func for accepting something func (f *File) Accept(v Visitor) { v.visit(f) } func (f *File) toString() string { return fmt.Sprintf("%s (%d)", f.getName(), f.getSize()) }visitor/element.go// Directory is sturct type Directory struct { name string dir []Entry } // NewDirectory func for initializing Directory func NewDirectory(name string) *Directory { return &Directory{ name: name, } } func (d *Directory) getName() string { return d.name } func (d *Directory) getSize() int { size := 0 for _, f := range d.dir { size += f.getSize() } return size } // Add func for adding directory func (d *Directory) Add(entry Entry) { d.dir = append(d.dir, entry) } // Accept func for accepting something func (d *Directory) Accept(v Visitor) { v.visit(d) } func (d *Directory) toString() string { return fmt.Sprintf("%s (%d)", d.getName(), d.getSize()) } func (d *Directory) getDir() []Entry { return d.dir }(5) ObjectStructure(オブジェクトの構造)の役
ObjectStructure
役は、Element
役の集合を扱う役です。ConcreteVisitor
役が個々のElement
役を扱えるようなメソッドを備えています。
サンプルプログラムでは、Directory
構造体がこの役を努めます。(一人二役です)(6) Client(依頼人)の役
サンプルプログラムでは、
startMain
関数が、この役を努めます。Main.gopackage main import ( "fmt" "./visitor" ) func startMain() { fmt.Println("Making root entries") rootdir := visitor.NewDirectory("root") bindir := visitor.NewDirectory("bin") tmpdir := visitor.NewDirectory("tmp") usrdir := visitor.NewDirectory("usr") rootdir.Add(bindir) rootdir.Add(tmpdir) rootdir.Add(usrdir) bindir.Add(visitor.NewFile("vi", 10000)) bindir.Add(visitor.NewFile("latex", 20000)) rootdir.Accept(visitor.NewListVistor()) fmt.Println("") fmt.Println("Making user entries") yuki := visitor.NewDirectory("yuki") hanako := visitor.NewDirectory("hanako") tomura := visitor.NewDirectory("tomura") usrdir.Add(yuki) usrdir.Add(hanako) usrdir.Add(tomura) yuki.Add(visitor.NewFile("diary.html", 100)) yuki.Add(visitor.NewFile("composite.py", 200)) hanako.Add(visitor.NewFile("memo.tex", 300)) tomura.Add(visitor.NewFile("game.doc", 400)) tomura.Add(visitor.NewFile("junk.mail", 500)) rootdir.Accept(visitor.NewListVistor()) fmt.Println("") fmt.Println("Occurring Exception...") tmpfile := visitor.NewFile("tmp.txt", 100) bindir = visitor.NewDirectory("bin") tmpfile.Add(bindir) } func main() { startMain() }(7) その他
エラー時の振る舞いを追加します
visitor/element.gofunc doError() error { msg := "FileTreatmentException" return fmt.Errorf("%s", msg) }■ 参考URL