1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

具体的には、コンパイラの最適化によって関数がインライン化される場合です。

インライン化

関数呼び出しには呼び出し命令分のオーバーヘッドがかかるため、本体の処理を呼び出し元に直接記載することで高速化できます。コンパイラが下記変換を自動で行うのがインライン化です。

before
func SayHi() {
	fmt.Println("Hi")
}

func main() {
	SayHi()
}
after
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するように改修した方が早いかもしれません

  1. インライン化はコンパイル時、モンキーパッチは実行時であることに注意してください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?