TL; DR
- コンパイラの最適化によって
gomonkey.ApplyFunc
が効かなくなることがある -
-gcflags=all=-l
で最適化を無視することでテスト可能
はじめに
gomonkey は、関数をモンキーパッチすることができるモジュールです。ユニットテストでHTTP通信/DBアクセス関数等をモックに差し替えることで、再現しづらいコーナーケースもテストできるようになります。
モンキーパッチではunsafeを用いたメモリ書き換えを行っています。プロダクトコード側への使用は非推奨です。
本記事では、モンキーパッチの関数 ApplyFunc
が動作しなかった際の原因と対処法について紹介します。
gomonkey.ApplyFunc
ApplyFunc
を使用すると、引数に渡した関数の処理を書き換えることができます。
// ターゲット関数
func Fact(n int) int {
if n <= 0 {
return 1
}
return n * Fact(n-1)
}
func main() {
fmt.Println(Fact(5))
// Factの処理を差し替える
patches := gomonkey.ApplyFunc(Fact, func(int) int {
return 99999999
})
defer patches.Reset()
fmt.Println(Fact(5))
}
120
99999999
仕組みとしては、unsafe
を使い関数ポインタを動的に書き換えることで代わりにモック関数が呼び出されるようにしています。
インライン化によりApplyFuncが動作しなくなる
しかし、下記のような場合にはモンキーパッチが効きません。
func SayHi() {
fmt.Println("Hi")
}
func main() {
SayHi()
patches2 := gomonkey.ApplyFunc(SayHi, func() {
fmt.Println("patched")
})
defer patches2.Reset()
SayHi()
}
Hi
Hi
具体的には、コンパイラの最適化によって関数がインライン化される場合です。
インライン化
関数呼び出しには呼び出し命令分のオーバーヘッドがかかるため、本体の処理を呼び出し元に直接記載することで高速化できます。コンパイラが下記変換を自動で行うのがインライン化です。
func SayHi() {
fmt.Println("Hi")
}
func main() {
SayHi()
}
func main() {
fmt.Println("Hi")
}
小さな関数、関数呼び出しを行わない関数ほどインライン化されやすいため、 上記 SayHi
は最適化の対象となっています。
そして、インライン化で置き換えられる処理は元の関数の処理なので、モンキーパッチが効かなくなってしまいます1。
patches2 := gomonkey.ApplyFunc(SayHi, func() {
fmt.Println("patched")
})
// SayHi() がインライン化
// べた書きの処理だからモンキーパッチできない!
fmt.Println("Hi")
インライン化されているか確認する
-gcflags="-m=1"
で、最適化情報をデバッグプリントできます。
$ go build -gcflags="-m=1" 2>&1 | grep inline
./main.go:16:6: can inline SayHi
./main.go:23:38: can inline main.func1
./main.go:26:2: can inline main.deferwrap1
./main.go:32:40: can inline main.func2
./main.go:35:2: can inline main.deferwrap2
SayHi
はインライン化され、Fact
は関数呼び出しのままであることが確認できます。
対処法
コンパイル時にインライン化しないようにすれば解消します。以下のフラグで最適化を無効化します。
$ go build -gcflags=all=-l
今度は想定通りモンキーパッチされました。
Hi
patched
おわりに
以上、モンキーパッチが効かない原因の紹介でした。原因が分かりづらいので厄介でした...
いっそのこと、モンキーパッチではなくDIするように改修した方が早いかもしれません
-
インライン化はコンパイル時、モンキーパッチは実行時であることに注意してください。 ↩