トーク

コード生成なしでモック処理を実現!ovechkin-dm/mockioで学ぶメタプログラミング
Room 1 17:30 - 17:50
本セッションでは、コード生成を必要とせずランタイムでモック処理を実現するovechkin-dm/mockio[1]というライブラリを取り上げ、Go言語におけるメタプログラミングの手法について解説します。
モックライブラリは単体テストにおける強力なツールです。
依存モジュールの振る舞いを一時的に置き換え、対象ロジックのテストを容易にします。
uber-go/mock[2]やmatryer/moq[3]といった既存のGoモックライブラリの多くは、事前のコード生成を前提とした設計になっています。
一方、本セッションで取り扱うmockioは、code1に示す通り、コード生成を一切必要とせずに型安全なモック処理を実現する革新的な手法を採用しています。
mockioの根幹を支えるのがovechkin-dm/go-dyno[4]です。
go-dynoは、インターフェース型を渡すとそれを満たす構造体を動的に生成するDynamic関数を提供します。
さらに、生成された構造体のメソッド呼び出し時に任意の処理を発火させるプロキシ関数(code2のCalculatorHandler参照)を渡すことが可能で、これがモック処理の肝になります。
本セッションでは、go-dynoがどのようにしてインターフェースの型情報から、それを満たす構造体を動的に生成しているのか、メソッドの中身を自由に定義することができるのかを深く掘り下げます。
コード生成なしで型安全なメタプログラミングを実現するテクニックについて、generics, reflect, unsafe, assemblyを活用した内部実装を詳細に解き明かします。
また、mockioのモックライブラリとしての価値にも言及します。
コード生成を行わないモックアプローチの機能的な・パフォーマンス的なメリットと課題について、既存モックライブラリと比較・評価します。
Goのランタイムの理解を深めたい方やモックライブラリ開発に興味がある方にとって、このセッションが有益な情報となれば幸いです。
code1: mockioを活用した単体テスト
```
// main.go
package main
type Calculator interface {
Add(a, b int) int
}
func Sum(calculator Calculator, nums ...int) int {
var result int
for _, num := range nums {
result = calculator.Add(result, num)
}
return result
}
// main_test.go
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/ovechkin-dm/mockio/v2/mock"
)
func Test_Sum(t *testing.T) {
ctrl := mock.NewMockController(t)
// NOTE: コード生成を行わずにモックを実現
mockCalculator := mock.Mock[Calculator](ctrl)
mock.When(mockCalculator.Add(0, 1)).ThenReturn(1)
mock.When(mockCalculator.Add(1, 2)).ThenReturn(3)
result := Sum(mockCalculator, 1, 2)
assert.Equal(t, 3, result)
mock.Verify(mockCalculator, mock.Times(1)).Add(0, 1)
mock.Verify(mockCalculator, mock.Times(1)).Add(1, 2)
}
```
code2: go-dynoによるメタプログラミング
```
package main
import (
"fmt"
"reflect"
"github.com/ovechkin-dm/go-dyno/pkg/dyno"
)
type Calculator interface {
Add(a, b int) int
Sub(a, b int) int
}
// NOTE: ランタイムで生成される構造体が持つメソッドの振る舞いをプロキシとして定義
func CalculatorHandler(m reflect.Method, values []reflect.Value) []reflect.Value {
fmt.Println("Method called:", m.Name)
switch m.Name {
case "Add":
return []reflect.Value{reflect.ValueOf(int(values[0].Int() + values[1].Int()))}
case "Sub":
return []reflect.Value{reflect.ValueOf(int(values[0].Int() - values[1].Int()))}
}
return nil
}
func main() {
// NOTE: Calculator型を満たす構造体をランタイムで生成
dynamicCalculator, _ := dyno.Dynamic[Calculator](CalculatorHandler)
addResult := dynamicCalculator.Add(2, 1) // Output: Method called: Add
fmt.Println(addResult) // Output: 3
subResult := dynamicCalculator.Sub(2, 1) // Output: Method called: Sub
fmt.Println(subResult) // Output: 1
}
````
[1]: https://github.com/ovechkin-dm/mockio
[2]: https://github.com/uber-go/mock
[3]: https://github.com/matryer/moq
[4]: https://github.com/ovechkin-dm/go-dyno