Slide 1

Slide 1 text

Dive into gomock id:utgwkk / @utgwkk (うたがわきき) 2024/06/08 Go Conference 2024

Slide 2

Slide 2 text

自己紹介 ● うたがわきき (@utgwkk) ● 株式会社はてな ○ Webアプリケーションエンジニア ○ 京都から来ました ● 好きなパッケージ ○ reflect

Slide 3

Slide 3 text

今日のおしながき ● interfaceを使ったモックについて復習する ● gomock/mockgenについて知る ● gomockを使ったテストの処理の流れを知る ● gomockの内部的な仕組みを知る

Slide 4

Slide 4 text

なぜinterfaceに依存するのか ● 実装の詳細をドメインロジックから切り離す ○ DBアクセス、HTTP APIコールなど ○ モックしてテストを書きやすくする ● interfaceを介して実装を差し替え可能にする ○ 動的な言語と異なり、あらゆるメソッドが最初から モック可能なわけではない

Slide 5

Slide 5 text

interfaceを定義する type UserStore interface { FindUserById( ctx context.Context, userId model.UserId, ) (*model.User, error) }

Slide 6

Slide 6 text

interfaceのメソッドを呼ぶ func (s *BlogService) RegisterNewBlog(...) { // ログインユーザーの存在確認 user, err := s.userStore.FindUserById( ctx, session.UserId, ) // ... }

Slide 7

Slide 7 text

テストを書くには? s := NewBlogService(...) // UserStore.FindUserById メソッドが、 // (*, model.UserId("user")) という引数で呼ばれることを期待 し、 // 呼ばれたら (&model.User{}, nil) を返す。 // もしメソッド呼び出しがなければテストは失敗する。 blog, err := s.RegisterNewBlog(...)

Slide 8

Slide 8 text

interfaceのモックを使ってテストする ● さまざまなライブラリ・流派がある ○ go.uber.org/mock (gomock, mockgen) ○ github.com/stretchr/testify/mock (mockery) ○ github.com/gojuno/minimock ○ 自作する ○ ほか

Slide 9

Slide 9 text

interfaceのモックを使ってテストする ● さまざまなライブラリ・流派がある ○ go.uber.org/mock (gomock, mockgen) ○ github.com/stretchr/testify/mock (mockery) ○ github.com/gojuno/minimock ○ 自作する ○ ほか

Slide 10

Slide 10 text

go.uber.org/mock (gomock) ● https://github.com/uber-go/mock ● モックフレームワーク ○ mockgen (後述) で生成したinterfaceのモック実装と 組み合わせて使う ● もともとはGoogleがメンテナンスしていた ○ Uberがメンテナを引き継いだ

Slide 11

Slide 11 text

mockgen ● gomockを使ったinterfaceのモック実装を生 成するコマンドラインツール ● go generateと組み合わせて利用可能 ○ //go:generate mockgen ...

Slide 12

Slide 12 text

gomockでテストを書く ctrl := gomock.NewController(t) m := mock_repo.NewMockUserStore(ctrl) s := NewBlogService(m, ...) m.EXPECT(). FindUserById(gomock.Any(), model.UserId("user")). Return(&model.User{}, nil) blog, err := s.RegisterNewBlog(...)

Slide 13

Slide 13 text

簡単ですね?

Slide 14

Slide 14 text

本当に簡単ですか?

Slide 15

Slide 15 text

gomockでテストを書く (再掲) ctrl := gomock.NewController(t) m := mock_repo.NewMockUserStore(ctrl) s := NewBlogService(m, ...) m.EXPECT(). FindUserById(gomock.Any(), model.UserId("user")). Return(&model.User{}, nil) blog, err := s.RegisterNewBlog(...)

Slide 16

Slide 16 text

魔法を解き明かす旅に出る ● さらっと書いたけど、どうやって実現されて いるのか気になる ● 1つずつ明らかにしていきましょう

Slide 17

Slide 17 text

Dive into gomock id:utgwkk / @utgwkk (うたがわきき) 2024/06/08 Go Conference 2024

Slide 18

Slide 18 text

今日のおしながき (真) ● mockgenが生成するコードの様子を見る ● モックに渡った引数を比較する実装を知る ● 呼び出しの過不足を検知する実装を知る ● リフレクションが活用されていることを知る

Slide 19

Slide 19 text

mockgen (再掲) ● gomockを使ったinterfaceのモック実装を生 成するコマンドラインツール ● go generateと組み合わせて利用可能 ○ //go:generate mockgen ...

Slide 20

Slide 20 text

mockgenの実行モード ● 2つの実行モードがある ○ source mode ○ reflect mode

Slide 21

Slide 21 text

source mode ● 指定されたファイルのinterface定義を静的解 析して、モック実装を生成する ○ 比較的高速 ○ 生成対象はファイル単位でしか指定できない

Slide 22

Slide 22 text

reflect mode ● リフレクションを用いてinterface定義を解析 し、モック実装を生成する ○ 生成対象のinterfaceを柔軟に指定できる ■ 他ライブラリのinterfaceも対象にできる ○ パッケージのコンパイルが必要 ■ source modeよりも遅くなりがち

Slide 23

Slide 23 text

interfaceのモックを生成する // 以下のinterfaceのモック実装を生成することを考える type UserStore interface { FindUserById( ctx context.Context, userId model.UserId, ) (*model.User, error) }

Slide 24

Slide 24 text

interfaceのモックを生成する //go:generate mockgen -source=user_store.go -package mock_store -destination mock_store/user.go

Slide 25

Slide 25 text

生成されるコードの様子を見る ● いったん雰囲気だけ見ていきましょう…… ● あとで詳細を解説します

Slide 26

Slide 26 text

生成されるコードの様子を見る

Slide 27

Slide 27 text

生成されるコードの様子を見る

Slide 28

Slide 28 text

生成されるコードの様子を見る

Slide 29

Slide 29 text

生成されるコードの様子を見る ● いったん雰囲気だけ見た ● あとで詳細を解説します

Slide 30

Slide 30 text

Matcher ● 引数が満たすべき「述語」に相当する ● メソッドに渡された引数が正当であるかどう か検査するためのinterface

Slide 31

Slide 31 text

Matcherに要求されるメソッド type Matcher interface { Matches(x any) bool String() string }

Slide 32

Slide 32 text

Matches(x any) bool ● メソッドに渡された引数が、matcherの期待 する条件を満たしているかどうか判定する ● falseを返したらテストが失敗する

Slide 33

Slide 33 text

String() string ● matcherが期待する引数の条件を表す文字列 表現を返す ● 期待する引数が渡されず、テストが失敗した ときに印字される

Slide 34

Slide 34 text

テスト失敗時の出力例 user.go:103: Unexpected call to *user_test.MockIndex.Put([a 1]) at user.go:103 because: expected call at user_test.go:17 doesn't match the argument at index 0. Got: a (string) Want: is equal to c (string)

Slide 35

Slide 35 text

組み込みのMatcher ● Any() ● Nil() ● Eq(x any) ● All(ms ...Matcher) ● AnyOf(xs ...any) ● Len(i int) ● Not(x any) ● Cond(fn func(x any) bool) ● AssignableTypeOf(x any) ● InAnyOrder(x any) ● Regex(regexStr string)

Slide 36

Slide 36 text

組み込みのMatcher ● Any() ○ 任意の引数を受理する ● Nil() ○ nilを受理する ● Eq(x any) ○ xに等しい引数を受理する

Slide 37

Slide 37 text

組み込みのMatcher ● All(ms ...Matcher) ○ 引数に渡したMatcher全てが受理するなら受理する ● AnyOf(xs ...any) ○ 引数のいずれかに一致するなら受理する ● Len(i int) ○ 引数のlenが一致するなら受理する

Slide 38

Slide 38 text

組み込みのMatcher ● Not(x any) ○ 引数がxでないなら受理する ● Cond(fn func(x any) bool) ○ 引数をfnに渡してtrueが返るなら受理する ○ 任意の条件が書ける ● AssignableTypeOf(x any) ○ 引数が型xに代入可能なら受理する

Slide 39

Slide 39 text

組み込みのMatcher ● InAnyOrder(x any) ○ 引数がxの要素の順序を無視して一致するなら受理す る ● Regex(regexStr string) ○ 引数が正規表現regexStrにマッチする文字列もしくは バイト列なら受理する

Slide 40

Slide 40 text

デフォルトのMatcher ● 期待する引数をMatcherでラップしなかった 場合、gomock.Eq() でラップされる ● つまり以下2つの式は等価 ○ m.EXPECT().Foo(1).Return(2) ○ m.EXPECT().Foo(gomock.Eq(1)).Return(2)

Slide 41

Slide 41 text

Matcherの実装例: Len(i int) ● 引数のlenを確かめる Matcher ● 引数をreflect.Valueに変換 する ● lenが取得できるなら ○ lenが期待した値と一致する ことを確かめる ● lenが取得できないなら ○ falseを返す

Slide 42

Slide 42 text

Matcherは合成可能 ● Matcherの引数にMatcherを渡せる ○ AnyOf(Eq(1), Nil()) とか ● Matcherを組み合わせることで、複雑なマッ チ条件を表現できる

Slide 43

Slide 43 text

Matcherをラップする関数 ● Matcherが表す述語は変えずに機能を足す ● WantFormatter(s fmt.Stringer, m Matcher) ● GotFormatterAdapter(s GotFormatter, m Matcher)

Slide 44

Slide 44 text

WantFormatter ● テストに失敗したときの「期待する引数」が どのように出力されるのかを制御する

Slide 45

Slide 45 text

WantFormatterの使われ方 ● 期待する引数は fmt.Sprintf("%v") でフォー マットされる ○ fmt.Stringerを実装していたらString()メソッドが呼 ばれる

Slide 46

Slide 46 text

WantFormatter の実装 ● MatcherのMatchesメ ソッドとfmt.Stringer を合成する ○ embed struct field を駆使している ○ テクい

Slide 47

Slide 47 text

GotFormatter ● テストに失敗したときの「渡された引数」が どのように出力されるのかを制御する ● 関数をGotFormatterAdapterでラップして 作るのが簡単 ○ func(got any) string

Slide 48

Slide 48 text

GotFormatterの使われ方 ● GotFormatterで文字列表現を得る ○ デフォルトでは %v (%T) という形式になる

Slide 49

Slide 49 text

GotFormatterAd apterの実装 ● 非常にシンプル

Slide 50

Slide 50 text

gomock.Controller ● gomockを使ったテストの中心となる型 ○ gomock.NewController(t) で生成する ○ モック実装のコンストラクタに渡す

Slide 51

Slide 51 text

テストの終了を フックする ● 何もしなくても勝手に フックされる ○ t.Cleanup() ○ Go 1.14以前は ctrl.Finish() を自分 で呼ぶ必要があった ■ 現在は不要

Slide 52

Slide 52 text

メソッド呼び出しが足り なかったらテストを失敗 させる ● ctrl.finish() の実装 ● 期待する呼び出しを全 て満たしていなかった ら t.Fatalf() を呼ぶ

Slide 53

Slide 53 text

期待するメソッド呼び出しを登録する ● mock.EXPECT().MethodName(...) ● ctrl.RecordCallWithMethodType() が呼ば れる ○ 具体例で見ていきましょう

Slide 54

Slide 54 text

期待するメソッド呼び出しを登録する m.EXPECT(). FindUserById( gomock.Any(), model.UserId("user"), ). Return(&model.User{}, nil)

Slide 55

Slide 55 text

RecordCallWithMethodType()

Slide 56

Slide 56 text

RecordCallWithMethodType() の実 装

Slide 57

Slide 57 text

ctrl.expectedCal ls.Add() ● (レシーバ, メソッド名) の組をキーとして、期 待するメソッド呼び出 しの一覧を記録する

Slide 58

Slide 58 text

gomock.Call ● モックinterfaceにおける「メソッド呼び出 し」を表す型

Slide 59

Slide 59 text

メソッドの呼び出し回数をテストする ● Times(n) ○ ちょうどn回呼ばれる必要がある ● MaxTimes(n) ○ 最大でn回まで呼ばれてよい ● MinTimes(n) ○ 最小でもn回呼ばれる必要がある ● AnyTimes() ○ 何度呼ばれてもよい (呼ばれなくてもよい)

Slide 60

Slide 60 text

メソッドの振る舞いを規定する ● Return() ● DoAndReturn() ● Do() ● SetArg()

Slide 61

Slide 61 text

Return() ● メソッドが呼ばれたときの返り値を定義する

Slide 62

Slide 62 text

DoAndReturn() ● 渡された関数をラップするような関数をリフ レクションで生成して登録する ● 返り値を返せる

Slide 63

Slide 63 text

Do() ● 渡された関数をラップするような関数をリフ レクションで生成して登録する ● 返り値は無視される

Slide 64

Slide 64 text

SetArg(n, value) ● メソッドに渡されたn番目の引数の値をvalue に代入する ● 渡された引数をあとで使うことができる ○ けど筆者は使ったことない

Slide 65

Slide 65 text

メソッドの呼び出し順を規定する ● メソッドA→メソッドB の順に呼ばれることを テストで確認したい ○ 何も指定しない場合、メソッドの呼び出し順は自由 ● 実現方法は2つ ○ call.After() メソッドを使う ○ gomock.InOrder() 関数を使う

Slide 66

Slide 66 text

call.After() メソッド ● callB.After(callA) ○ callA が呼ばれた後に callB が呼ばれる ● 呼び出し順を依存グラフとして保持 ○ 複雑なテストパターンも表現できる

Slide 67

Slide 67 text

call.After() メソッド

Slide 68

Slide 68 text

gomock.InOrder() 関数 ● gomock.InOrder(callA, callB, ...) ○ call.After() を順に呼び出している ● 典型的な場合は gomock.InOrder() の方が楽

Slide 69

Slide 69 text

gomock.InOrder() 関数

Slide 70

Slide 70 text

モックのメソッド呼び出しに応じる ● 登録済みのメソッド呼び出しの処理やアサー ションに移譲する ● これも具体例で見ていきましょう

Slide 71

Slide 71 text

モックのメソッド呼び出しに応じる

Slide 72

Slide 72 text

モックのメソッド 呼び出しに応じる ● さすがに小さすぎて見 えない ● 順を追っていきます

Slide 73

Slide 73 text

メソッド呼び出しの マッチングを行う ● FindMatch() ○ 登録済みのメソッド呼 び出しにマッチング ○ メソッド呼び出しが見 つからなかったらテス トを失敗させる ● FindMatch() の詳細は 後述

Slide 74

Slide 74 text

呼ばれる処理を 取ってくる ● 前提となるメソッド呼 び出しを解決済とする ● 呼び出し回数の上限に 達していたら ○ メソッド呼び出し一覧 から削除する

Slide 75

Slide 75 text

メソッドの処理を 実行して値を返す ● マッチした処理を順に 呼び出す ● 最後に呼ばれた処理の 返り値が、interfaceの モック実装のメソッド の返り値になる

Slide 76

Slide 76 text

FindMatch() の実 装 ● これも小さくて見えな いと思う ● 順を追っていきましょ う……

Slide 77

Slide 77 text

期待するメソッド呼 び出しのマッチング ● call.matches(args) が 本質 ○ 後述します ○ 覚悟してください ● あとはエラーハンドリ ング ○ human-readableなエ ラーを出す工夫

Slide 78

Slide 78 text

call.matches(args) の実装 ● メソッド呼び出しのマッチング処理の本質

Slide 79

Slide 79 text

No content

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

matches() の実装 ● メソッド呼び出しのマッチング処理の本質 ● スライド3枚使っても文字が小さすぎる ○ コードを直接見ていきましょう…… ○ ここでVSCodeを開きます

Slide 83

Slide 83 text

matches() の実装を読みました ● おつかれさまでした

Slide 84

Slide 84 text

gomockでテストを書く (再掲) ctrl := gomock.NewController(t) m := mock_repo.NewMockUserStore(ctrl) s := NewBlogService(m, ...) m.EXPECT(). FindUserById(gomock.Any(), model.UserId("user")). Return(&model.User{}, nil) blog, err := s.RegisterNewBlog(...)

Slide 85

Slide 85 text

分かるようになりましたね? ctrl := gomock.NewController(t) m := mock_repo.NewMockUserStore(ctrl) s := NewBlogService(m, ...) m.EXPECT(). FindUserById(gomock.Any(), model.UserId("user")). Return(&model.User{}, nil) blog, err := s.RegisterNewBlog(...)

Slide 86

Slide 86 text

おわりに: ライブラリの実装を読む ● 今回はgomockの話をしたけど、そもそもな ぜライブラリの実装を読むのか ● 自分の場合は主に3つの目的がある ○ 知的好奇心を満たす ○ 自分の糧にする ○ 腕力をつける

Slide 87

Slide 87 text

知的好奇心を満たす ● 隠蔽された実装は限りなく魔法に近い ○ 十分に発達した科学技術は、魔法と見分けがつかない ○ 「魔法」の正体を解き明かす ● 仕組みが分かると嬉しい

Slide 88

Slide 88 text

自分の糧にする ● 知らないものを知らないままにしない ○ 知らないものは知らない (怖くて触れない) ○ 手札を増やす ● 学びがどんどん出てくるはず ○ 簡潔に・抽象度の高い形で実装するには? ○ 気をつけるべきポイント・落とし穴がないか?

Slide 89

Slide 89 text

腕力をつける ● ライブラリの内部実装が気になる瞬間がどう しても訪れる ○ 生きているとさまざまなことが…… ● いざというときに実装が読めないと、手が出 なくなる ○ 普段からライブラリの実装を読んでおく

Slide 90

Slide 90 text

まとめ ● interfaceのモックを使ったテスト手法を紹介 しました ● gomockの内部実装について紹介しました ○ リフレクションが活用されているのを見ました ● 好きなライブラリの実装を深掘りしてみませ んか

Slide 91

Slide 91 text

hatena.co.jp/recruit