Golang 单元测试之绕过 init panic

  1. init 函数中发生 panic
  2. 利用 init 默认加载的顺序解决
  3. Golang 的执行顺序

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


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 nickchenyx@gmail.com

Title:Golang 单元测试之绕过 init panic

Count:921

Author:nickChen

Created At:2022-06-02, 14:59:48

Updated At:2023-05-08, 23:27:10

Url:http://nickchenyx.github.io/2022/06/02/golang-init-panic-mock/

Copyright: 'Attribution-non-commercial-shared in the same way 4.0' Reprint please keep the original link and author.