haya14busa

haya14busa’s memo

Golangにおけるinterfaceをつかったテストで Mock を書く技法

いい記事に感化されて僕も何か書きたくなった。

Golangにおけるinterfaceをつかったテスト技法 | SOTA

リスペクト:

今週のやつではなく先週のです.今週のは特に知見がなかった…grpc-goとか使えたらクライアント勝手に生成されるしいいよねgrpc流行ると便利そう(感想) くらい

Golangにおけるinterfaceをつかったテスト技法 | SOTA めっちゃいいなーと思ったんですが,テスト用 の mock を気軽に作るテクニックはあまり詳しく紹介されてなかったのでそのあたりの1つのテクニックを書きたい.

前提

僕もテストフレームワークや外部ツールは全く使わない.標準のtestingパッケージのみを使う. testify もいらないし, mock するために gomock も基本はいらない.

とにかくGolangだけで書くのが気持ちがいい,に尽きる.

テスト用 fake client をつくる

全体の動くはずのgist: https://gist.github.com/haya14busa/27a12284ad74477a6fd6ed66d0d153ee

例えばこういう実装のテストを書くときのことを考えます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
  "context"
  "fmt"
)

type GitHub interface {
  CreateRelease(ctx context.Context, opt *Option) (string, error)
  GetRelease(ctx context.Context, tag string) (string, error)
  DeleteRelease(ctx context.Context, releaseID int) error
}

type GhRelease struct {
  c GitHub
}

func (ghr *GhRelease) CreateNewRelease(ctx context.Context) (*Release, error) {
  tag, err := ghr.c.CreateRelease(ctx, nil)
  if err != nil {
      return nil, fmt.Errorf("failed to create release: %v", err)
  }

  // check created release
  if _, err := ghr.c.GetRelease(ctx, tag); err != nil {
      return nil, fmt.Errorf("failed to get created release: %v", err)
  }

  // ...
  return &Release{}, nil
}

type Option struct{}
type Release struct{}

GitHub interface をテストでは mock したものを使いたい.そういうときには以下のように mock を作ると便利です.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type fakeGitHub struct {
  // インターフェース埋め込み
  GitHub
  FakeCreateRelease func(ctx context.Context, opt *Option) (string, error)
  FakeGetRelease    func(ctx context.Context, tag string) (string, error)
  // 埋め込みを使うので,例えば DeleteRelease はまだテストしないので mock
  // しない... いうことができる.
}

func (c *fakeGitHub) CreateRelease(ctx context.Context, opt *Option) (string, error) {
  return c.FakeCreateRelease(ctx, opt)
}

func (c *fakeGitHub) GetRelease(ctx context.Context, tag string) (string, error) {
  return c.FakeGetRelease(ctx, tag)
}

fakeGitHub という struct を作成し,インターフェースをとにかく満たすために GitHub interface を埋め込みます.

そして mock したいメソッドは新たに func (c *fakeGitHub) CreateRelease(...) (...) と 定義しなおし,実装の中身は fakeGitHub に持たせた FakeCreateRelease field に丸投げします.

このようにしてテスト用 mock を作るとそれぞれのテストで簡単に中身の実装を変えられるので大変便利です.

実際にテストしてみる例

main_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
  "context"
  "fmt"
  "testing"
)

type fakeGitHub struct {
  // インターフェース埋め込み
  GitHub
  FakeCreateRelease func(ctx context.Context, opt *Option) (string, error)
  FakeGetRelease    func(ctx context.Context, tag string) (string, error)
  // 埋め込みを使うので,例えば DeleteRelease はまだテストしないので mock
  // しない... いうことができる.
}

func (c *fakeGitHub) CreateRelease(ctx context.Context, opt *Option) (string, error) {
  return c.FakeCreateRelease(ctx, opt)
}

func (c *fakeGitHub) GetRelease(ctx context.Context, tag string) (string, error) {
  return c.FakeGetRelease(ctx, tag)
}

func TestGhRelease_CreateNewRelease(t *testing.T) {
  fakeclient := &fakeGitHub{
      FakeCreateRelease: func(ctx context.Context, opt *Option) (string, error) {
          return "v1.0", nil
      },
      FakeGetRelease: func(ctx context.Context, tag string) (string, error) {
          return "", fmt.Errorf("failed to get %v release!", tag)
      },
  }

  ghr := &GhRelease{c: fakeclient}

  release, err := ghr.CreateNewRelease(context.Background())
  if err != nil {
      t.Error(err)
      // => failed to get created release: failed to get v1.0 release!
  }
  _ = release
  // ...
}

以下のような感じで,簡単にテスト用mockの実装を書いて,テストすることができます.

1
2
3
4
5
6
7
8
fakeclient := &fakeGitHub{
  FakeCreateRelease: func(ctx context.Context, opt *Option) (string, error) {
      return "v1.0", nil
  },
  FakeGetRelease: func(ctx context.Context, tag string) (string, error) {
      return "", fmt.Errorf("failed to get %v release!", tag)
  },
}

上記の例では1種類の実装しかテストしてないのであまり恩恵がわかりづらいかも知れないですが, 例えば error が帰ってきたときに正しくエラーハンドリングできてるかとか, 返り値をいろいろ変えたものをいくつか作ってテストする…といったことが上記のパターンを 使うことによって簡単にできます.Table Testing することも可能.

普通にわざわざstructごと作っていると,例えばテストの関数ないでは struct の method (e.g. func (c *client) Func()) を定義することができません.

そこで FakeFunc func() というfield を持たせて実装を丸投げすることによって, 簡単にいろんな実装のテスト用 mock を作成してテストができるということの紹介でした.

まとめ

僕は最初にこのパターンを教わってなるほどなぁ…と思ったんですが,いざ世にでてみると(?) ぜんぜんこのパターンを紹介しているものが見つからなかったので紹介してみました. (一応どっかの medium の英語記事にこれに似たパターンが紹介されてたのを見た気もする…)

ぜひ使ってみてください.

あまり関係ない追記

この記事の主旨とは関係ないけど,基本的にテスト用ライブラリは使わないとはいえ, たまににヘルパー関数ほしいなーというケースがあります.

でかい struct をテストで比較するときに,比較自体は reflect.DeepEqual で出来るのだけど, もし違っていたときにどこが違うかを表示するのが面倒くさいのでヘルパー関数提供してくれるライブラリがほしい…

某社でgoのテスト書いてたときもこういう大きめのstruct比較するケースでは便利diff表示用ライブラリを 使っていた気がしたんだけど,なんかOSSで見つからない気がする… prettycmp みたいな名前だった気がするが どうだったか… そもそも記憶違いな気もする…

追記: twitter で教えてもらいましたが https://github.com/kylelemons/godebug っぽいです. 便利. https://godoc.org/github.com/kylelemons/godebug/pretty#Compare

Comments