init 函数中发生 panic
在做单元测试的时候,当程序引入的包内有 init 函数并且抛出了 panic,如何修复这种场景?
第一反应肯定是能否 mock 掉出问题的函数,但是因为 mock 的执行顺序是在依赖包的 init 执行之后,所以 mock 生效前,init 函数就已经 panic 了。明显这样做是无效的。
以往的经验是在发生 panic 的 init 函数代码仓库中,新建一个测试分支,修改 init 中的逻辑避免 panic。然后再在单测代码中引入这个修复分支,才可以进行测试。在这里提供一个新的思路↓
利用 init 默认加载的顺序解决
新的方案,利用 init 加载顺序的机制,在前置运行的 init 函数中,mock 会发生 panic 的 init 函数场景。
我们尝试使用 init 加载顺序修复这个问题,如下的三个项目:
- import_panic_init 这个是苦主项目,他的引用了会产生 panic 的 init 函数的外部包
- panic_init 会产生 panic 的 init 的函数所在地
- util 无辜的工具库,被 panic_init 错误的使用产生了 panic
代码可以在仓库中获取:https://github.com/nickChenyx/code-repo/tree/main/golang/test_panic_init. ├── import_panic_init │ ├── a │ │ └── a.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── panic_init │ ├── go.mod │ └── panic_init.go └── util ├── go.mod └── util.go
在 util 包中,util.go 文件如下:
package util
import "fmt"
func IsTest(t *int) bool {
fmt.Println("util.isTestCall")
if t == nil {
panic("t can't be nil")
}
return *t == 0
}
在 panic_init 包中,panic_init.go 文件如下:
package panic_init
import "fmt"
import "util"
func init() {
util.IsTest(nil) // 必定发生 panic
fmt.Println("panic_init run..")
}
在 import_panic_init 包中,main.go 文件如下:
package main
import (
"fmt"
_ "panic_init"
"util"
"bou.ke/monkey"
)
func main() {
monkey.Patch(util.IsTest, func(t *int) bool {
return true
})
fmt.Println("main run...")
}
可以看到此处 main 函数妄图 mock util.IsTest 函数,避免 panic 影响 fmt.Println(“main run…”) 的执行。
但是运行结果是:
$ go run main.go # 执行 import_panic_init.main 函数
util.isTestCall
panic: t can't be nil
goroutine 1 [running]:
util.IsTest(0x0)
.../projects/test_panic_init/util/util.go:8 +0x89
panic_init.init.0()
.../projects/test_panic_init/panic_init/panic_init.go:7 +0x1b
exit status 2
可以看到 main 函数中的 mock 实际上未生效。修改 main.go 文件如下:
package main
import (
_ "a" // 添加了这个 a 包,并且在 panic_init 包之前引入!这很重要
"fmt"
_ "panic_init"
"util"
"bou.ke/monkey"
)
func main() {
monkey.Patch(util.IsTest, func(t *int) bool {
return true
})
fmt.Println("main run...")
}
在 import_panic_init/a/a.go
中,定义了 mock 函数用于 mock util 包的函数如下:
package a
import "util"
import "bou.ke/monkey"
import "fmt"
func init() {
monkey.Patch(util.IsTest, func(t *int) bool {
return true
})
fmt.Printf("a init call util.IsTest: %v\n", util.IsTest(nil))
}
然后再执行 main.go 如下:
$ go run main.go # 执行 import_panic_init.main
a init
a init call util.IsTest: true
panic_init run..
main run...
可以看到此时 a 包的 init 先于 panic_init 包的 init 执行,所以 mock 函数先被执行,panic_init 中的 util.IsTest 调用被 mock 返回 true,而不会发生 panic!
有趣的是,如果将 main.go 中 import 的顺序调整,那么依然会发生 panic:
import (
"fmt"
_ "panic_init"
_ "a" // 在 panic_init 包之后引入,此时执行顺序在 panic_init 包之后
"util"
"bou.ke/monkey"
)
Golang 的执行顺序
import --> const --> var --> init()
一个 golang 文件中执行的顺序如上,先执行文件中定义的 import 包中的逻辑,再执行 const 常量定义,再执行 var 变量定义,再执行 init 函数。
具体可看:https://learnku.com/go/t/47135
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [email protected]