init 函数中发生 panic

在做单元测试的时候,当程序引入的包内有 init 函数并且抛出了 panic,如何修复这种场景?

第一反应肯定是能否 mock 掉出问题的函数,但是因为 mock 的执行顺序是在依赖包的 init 执行之后,所以 mock 生效前,init 函数就已经 panic 了。明显这样做是无效的。

以往的经验是在发生 panic 的 init 函数代码仓库中,新建一个测试分支,修改 init 中的逻辑避免 panic。然后再在单测代码中引入这个修复分支,才可以进行测试。在这里提供一个新的思路↓

利用 init 默认加载的顺序解决

新的方案,利用 init 加载顺序的机制,在前置运行的 init 函数中,mock 会发生 panic 的 init 函数场景。

我们尝试使用 init 加载顺序修复这个问题,如下的三个项目:

  1. import_panic_init 这个是苦主项目,他的引用了会产生 panic 的 init 函数的外部包
  2. panic_init 会产生 panic 的 init 的函数所在地
  3. util 无辜的工具库,被 panic_init 错误的使用产生了 panic
    .
    ├── import_panic_init
    │   ├── a
    │   │   └── a.go
    │   ├── go.mod
    │   ├── go.sum
    │   └── main.go
    ├── panic_init
    │   ├── go.mod
    │   └── panic_init.go
    └── util
     ├── go.mod
     └── util.go
    代码可以在仓库中获取:https://github.com/nickChenyx/code-repo/tree/main/golang/test_panic_init

在 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]