20200403のGoに関する記事は2件です。

[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.html

flextimeはテストコードの中で現在時刻を切り替えるためのライブラリです。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)に置き換えるかどうかは考えた方が良いですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golangで、デザインパターン「Visitor」を学ぶ

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Visitor」を学ぶ"

今回は、Pythonで実装した”Visitor”のサンプルアプリをGolangで実装し直してみました。

■ Visitorパターン(ビジター・パターン)

Visitorパターンは、オブジェクト指向プログラミング およびソフトウェア工学 において、 アルゴリズムをオブジェクトの構造から分離するためのデザインパターンである。分離による実用的な結果として、既存のオブジェクトに対する新たな操作を構造を変更せずに追加することができる。
基本的には Visitorパターンは一群のクラスに対して新たな仮想関数をクラス自体を変更せずに追加できるようにする。そのために、全ての仮想関数を適切に特化させた Visitor クラスを作成する。Visitorはインスタンスへの参照を入力として受け取り、ダブルディスパッチを用いて目的を達する。
Visitor は強力であるが、既存の仮想関数と比較して制限もある。各クラス内に小さなコールバックメソッドを追加する必要があり、各クラスのコールバックメソッドは新たなサブクラスで継承することができない。

UML class and sequence diagram

W3sDesign_Visitor_Design_Pattern_UML.jpg

UML class diagram

Visitor_design_pattern.svg.png

□ 備忘録

書籍「増補改訂版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.go
package 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.go
package 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.go
package 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.go
func doError() error {
    msg := "FileTreatmentException"
    return fmt.Errorf("%s", msg)
}

■ 参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む