私が歌川です

@utgwkk が書いている

gomock (mockgen) v0.5.0がリリースされた & package modeが導入された

はじめに

2024/10/18*1にgomock (mockgen) v0.5.0がリリースされました。

github.com

リリースノートを見ていこうと思うのですが、その前にpackage modeという新しいコード生成のモードについて触れましょう。

reflect modeの限界とpackage modeについて

mockgen v0.4.0までは、コード生成のモードとしてsource modeとreflect modeの2つがありました。

source modeは、指定されたGoのソースファイルを構文解析して得られた情報もとにinterfaceのモック生成を行うモードです。モック生成の対象がファイル単位でしか指定できない・別のpackageのinterfaceモックが生成できない など制約は強いけど、静的解析だけで済むので高速にコード生成できるモードです。このモードはmockgen v0.5.0にも残っています。

reflect modeは、リフレクションを使ってinterfaceのモック生成を行うモードです。リフレクションが使えるのでsource modeより柔軟性が高いように見えて、じつはいくつか限界がありました。

代表的なものに、ジェネリックな型を返すメソッドを含むモックのコード生成がエラーになる、というものがあります。reflect modeで以下のようなinterfaceのモックを生成するとエラーになります。

github.com

type Interface interface {
  GetSomeInt() *Some[int] // これが生成できない
}

type Some[T any] struct {
  // 省略
}

ほかにもいくつか限界がありました*2が、これらを解決するためにreflect modeとは仕組みの異なるコード生成モードを導入した、というのがpackage modeのモチベーションになります。

package modeは、golang.org/x/tools/go/packagesを使ってコードの解析を行ってinterfaceのモックを生成するモードになります。mockgen v0.5.0で導入されました。

github.com

package modeはreflect modeとの互換性を保ちつつ、reflect modeでは解決できてなかったコード生成の問題も解決しています。リフレクションではなく、golang.org/x/tools/go/packages というパッケージで提供されるGoのコード解析の仕組みを使っています。golang.org/x/tools/go/packages については以下の記事が詳しいです。

int128.hatenablog.com

リリースノート

package modeについて見たので、リリースノートのほうも見てみましょう。

Added

#153: Add --write_command_comment flag to specify whether to include Generated by this command comment.

github.com

mockgenでコード生成をすると、生成されたコードに以下のようなコメントが出力されます。

// Generated by this command:
//
// mockgen -mock_names=Service=UserServiceMock -package mocks -typed -destination mocks/user_service.go -self_package go.uber.org/mock/mockgen/internal/tests/mock_name/mocks go.uber.org/mock/mockgen/internal/tests/mock_name/user Service

この挙動はv0.4.0まではコマンドライン引数で制御できなかったのですが、v0.5.0ではこのコメントを出力しないように --write_command_comment=false コマンドライン引数で設定できるようになりました。

コード生成時に実行されるコマンドは環境依存で、生成されるコードに余計な差分が入ってしまうから無効にできるようにしよう、というのが元のPRのモチベーションです。ワイトもそう思います。

bulkmockgenを作った側としては、モックするinterfaceを追加したときにこのコメントがコンフリクトして困っていました。コード生成後にこのコメントを取り除くスクリプトを回すようにしてなんとかしていましたが、コマンドライン引数が導入されたことでスクリプトの用もなくなって嬉しいですね。

#191: Add --build_constraint flag to add //go:build directives to generated mocks

github.com

生成されたコードに対して --build_constraint コマンドライン引数でbuild constraintを指定できるようにした、という変更です。

mockgenで生成したコードは基本的にテストでしか使わないけど、パッケージが分かれているのでプロダクションコードからimportしてしまう可能性がある、なのでbuild constraintで保護できるようにした、というモチベーションのようです。

github.com

たまに思うけど、テストを走らせるときだけ有効になるbuild tagとかないのかな。

#214: Add gob mode to support custom package loading techniques in place of --exec_only

github.com

reflect modeがpackage modeに代替されたことで、interfaceの情報を得るためにコードをコンパイルして実行する必要がなくなりました。そのため --exec_only コマンドライン引数が削除されたのですが、これだと困る場合があるようです。mockgenによるコード生成結果をうまく制御するために、mockgenのinterfaceの解析結果の内部表現をgob encodingしたものを得て、コネコネしたのちmockgenに渡す、ということが行われていたようです。ここまでやったことがないのでまだモチベーションが掴みきれていません。

この問題を解決するために、--model_gob というコマンドライン引数経由で、内部表現をgob encodingしたもののファイルパスを渡せるようにした、ということのようです。

Changed

#181: Made mockgen faster by changing flags passed to go list.

github.com

mockgenでコード生成するときに go list -json コマンドが叩かれていますが、引数を調整することでコード生成を高速にした、という変更です。

go list -json だけだと依存パッケージの解析なども行われますが、必要なのは ImportPathName だけなので無駄だった、ということですね。

#183: Made Cond matcher generic.

github.com

Cond matcherがジェネリックになりました。godocの差分*3を見るのが一番分かりやすいでしょう。

// これまで
Cond(func(x any){return x.(int) == 1}).Matches(1) // returns true
Cond(func(x any){return x.(int) == 2}).Matches(1) // returns false

// これから
Cond(func(x int){return x == 1}).Matches(1) // returns true
Cond(func(x int){return x == 2}).Matches(1) // returns false

ジェネリックでない既存のコードは型引数が常に any 型に推論されたものだとみなせるので、互換性は保たれています。

#204: Removed ISGOMOCK() from generated mocks.

github.com

生成されるモックに ISGOMOCK() メソッドを生やすのではなく、isgomock というプライベートなフィールドを含めるようにする、という変更です。この差分だけ見てもモチベーションが一見分かりづらいと思いますが、#144 と併せて見ることで謎が解けます。

#207: Deprecated reflect mode and replaced it with the new package mode.

github.com

reflect modeに対応するモードをまったく別の仕組みで書き直して、package modeという名前で導入した、という変更です。

記事の冒頭で触れたように、これが今回のリリースで一番の目玉じゃないでしょうか。

Fixed

#144: Fix a deadlock that can happen when mocking an interface that matches fmt.Stringer.

github.com

fmt.Stringer interfaceに対するモックがdeadlockを引き起こしうる問題が解決されました。

fmt.StringerString() メソッドがモックされていると、モック実装を文字列化する → String() メソッドが呼ばれる → モック実装が適切に呼ばれているかどうかの判定が走る → …… という無限ループになる、ということですかね。さもありなん……。

解決策として、モック実装を文字列化するときは %v ではなく %T というフォーマットで文字列化する、という形を取っています。モック実装かどうかを判定するために ISGOMOCK() メソッドの存在を確認していましたが、これだと名前衝突が起こる可能性が残っています。ということで、メソッドではなくプライベートなフィールドを使って判定するように変更した、というのが #204 なんですね。

#168: Fix an issue where the "generated by" comment was being included in the package comment of generated mocks.

github.com

パッケージ単位のコメントとして // Code generated by MockGen. DO NOT EDIT. から始まる行を含めないようにする、という変更です。差分は非常にシンプルですね。

おわりに

より便利になり、package modeの導入によって限界も超えたgomock (mockgen) v0.5.0がリリースされました。みなさまもどんどん最新バージョンを使っていきましょう。

*1:リリースノート上の日付は2024/10/15

*2:interfaceの型エイリアスに対するモックを生成できない、メソッドの引数名の情報が失われる、など

*3:https://github.com/uber-go/mock/pull/183/files#diff-3e04df284e7d2b35b623a74eb8e9ef27d7637304e4643f7c56256ece54f6b63eL342-R348