【Golang】测试函数的 “os.Exit(1)” 错误退出(测试及类似于在另一个进程中执行或模拟)
我想在Go语言(以下简称golang)的testing中测试函数是否会执行os.Exit(1)。
func ExitIfNotZero(status int) {
if status > 0 {
os.Exit(status)
}
}
然而,一旦被呼叫考试就会结束。
我在谷歌上搜索「”golang” test “os.Exit(1)”」,却没有找到恰好的文章,所以只是为了测试一下我的谷歌搜索能力。
总结一下(今北产业)
-
- 将函数赋值给变量并使用。推荐将os.Exit()赋值给变量并使用,以便在测试中替换函数。这种方法被称为“Monkey Patch”(猴子补丁)?。
main.go
// osExit是`os.Exit`的副本。用于方便测试os.Exit函数。
var osExit = os.Exit
// ExitIfNotZero在status不为0时以os.Exit(status)结束。
func ExitIfNotZero(status int) {
if status > 0 {
osExit(status)
}
}
main_test.go
package main
import (
“os”
“testing”
)
func TestExitIfNotZero(t *testing.T) {
// 备份osExit并在defer中恢复
oldOsExit := osExit
defer func() { osExit = oldOsExit }()
// 用于在OsExit中捕获退出状态的变量
var capture int
// 模拟(将函数分配给虚拟函数以更改行为)osExit。
//
// 【详细信息】
// 实际上,只是将os.Exit(code)赋值给capture。
// 如果capture的值发生变化,则说明已经到达osExit。
//
// 这种Monkey Patching(将函数分配给变量并在测试中替换的虚拟方法)可用于任何地方,如os.Stdin、os.Args、json.Marshal等。
//
// 它依赖于所使用模块函数的行为,如果需要在测试中更改行为,则最好使用它的副本。
// 这实际上是接近依赖注入的思想。
//
// 【注意】在测试中模拟(mock)全局变量的测试中将不能使用t.Parallel()进行并行测试。
osExit = func(code int) { capture = code }
// 测试使用的参数数据
testStatus := 10
// 执行被测试的函数
ExitIfNotZero(testStatus)
// 断言(验证预期结果)
// 如果capture的值不是0(初始值),而是预期的值,那么就说明已经测试到了ExitIfNotZero中的OsExit,并且这也会反映在代码覆盖率中。
expect := testStatus // 10
actual := capture
if expect != actual {
t.Errorf(
“断言失败。预期结果:%v,实际结果:%v”,
expect, actual,
)
}
}
在线预览(无注释版本)@ Go Playground
使用环境变量作为标志位并使用。在测试中,根据环境变量的标志位区分是否执行相应的函数,并在单独的进程中执行并获取状态。
在线预览示例 @ Go Playground
在上述“使用环境变量作为标志位并使用”的情况下,不会反映在代码覆盖率中,所以请注意。要在单独的进程中实现100%的代码覆盖率,需要做出一些努力。(正在准备相关文章。请参考评论)
在线预览100%代码覆盖率的示例 @ paiza.IO
使用os.Exit()后,我遇到了测试困难的问题。
在Go中,除了在退出应用程序时以外,一般不建议使用”os.Exit()”。特别是在使用goroutine和defer的情况下。因为当使用os.Exit()时,应用程序会立即终止,defer语句就无法执行。
在这里,假设以下情况:只在main()函数内使用os.Exit(),并让其他层级的函数通过返回错误来方便进行测试。
主函数示例:
func main() {
if err := run(); err != nil {
fmt.Fprint(os.Stderr, err.Error())
os.Exit(1)
}
}
run函数示例:
func run() error {
// 做一些操作
return errors.New(“something went wrong”)
}
我想对于使用Golang的单元测试功能testing中使用os.Exit(1)的函数进行动作测试。
由于Golang没有像try-catch-finally这样的异常处理方式,因此通常情况下不会抛出错误,而是通过返回值返回error。在这种情况下,据说实现我这样的error接口并将其作为返回值返回会更加灵活。
Go言語のエラーハンドリングについて @ Qiita
例外(exception)がない理由は? | FAQ @ Golang.jp
然而,我本以为这只是为了测试一个简单的 os.Exit(1) 函数的运行而实施的,但从中国的论坛上得到了一条信息,称它在2014年的Google I/O大会上被提及了。
使用「启用独立进程运行」这种惊人的方法,我突然恍然大悟。有了这个方法,我们可以将测试辅助函数应用于以 t.Fatalf() 来结束的测试案例,也就是测试自身的测试。
那么,我想在另外一个进程中执行测试来分开测试。
在另一个进程中执行测试。
简单示例
以下是一个测试样例,用于测试sayonara()在状态1(os.Exit(1))下是否正常退出。测试中它会递归调用自身,并通过环境变量来改变其功能和行为。
package main
import (
"fmt"
"os"
)
// この関数の動作テストがしたい
func sayonara() {
fmt.Println("Sayonara!")
os.Exit(1)
}
func main() {
sayonara()
}
package main
import (
"os"
"os/exec"
"testing"
)
// 再帰的に Test_sayonara を呼び出す
func Test_sayonara(t *testing.T) {
// 環境変数 "FLAG_RUN_SAYONARA" のフラグが立っていた場合に sayonara() を実行させる
if os.Getenv("FLAG_RUN_SAYONARA") == "1" {
sayonara()
return
}
// 外部プロセスの実行コマンド設定
var cmd = exec.Command(os.Args[0], "-test.run=Test_sayonara")
// 外部プロセス実行時の環境変数をセット(FLAG_RUN_SAYONARA -> 1)
cmd.Env = append(os.Environ(), "FLAG_RUN_SAYONARA=1")
// 外部プロセスの外部実行
var err = cmd.Run()
// 外部プロセスの実行結果取得。エラーの場合は正常終了
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return
}
// 実行ステータスがエラーでない(ステータスが 0)の場合は Fail させる
t.Fatalf("process ran with err %v, want exit status 1", err)
}
オンラインで動作をみる @ The Go Playground
問題在於,由於使用這種方法,因為流程不同,所以無法反映在代碼覆蓋率上,所以不能算作已網羅。實際上已網羅了。
根据评论,我们已经完成了涵盖100%的覆盖率的样本。但是由于缺乏动力,我们还在准备中,所以请等待文章的反映。我们将及时更新。
カバレッジ 100% のサンプルをオンラインでみる @ paiza.IO(いささか煩雑)
对 os.Exit() 进行模拟化
之后,在尝试了一些方法后,我在StackOverflow上找到了将os.Exit()函数进行模拟的方法。
Testing os.Exit scenarios in Go with coverage information (coveralls.io/Goveralls) @ StackOverflow
这种技巧不能用于模拟外部包的方法(例如结构体等对象函数),但如果是函数,则可以使用,因此更容易创建测试。特别是在不想导入模拟包只为了进行测试的情况下,可以简单地进行测试。
// ExitIfNotZero は引数が 0(ゼロ)より大きい場合に、その値のステータスで os.Exit() します.
func ExitIfNotZero(status int) {
if status > 0 {
os.Exit(status)
}
}
var OsExit = os.Exit // 関数を変数に代入させてモック(なんちゃって os.Exit)を作る
// ExitIfNotZero は引数が 0(ゼロ)より大きい場合に、その値のステータスで os.Exit() します.
func ExitIfNotZero(status int) {
if status > 0 {
OsExit(status) // モックを代わりに使う
}
}
这是利用了“函数也是数据类型,因此可以存储在变量中”的机制创建的。
関数 | 他言語プログラマがgolangの基本を押さえる為のまとめ @ Qiita
测试在这里。请注意,将函数分配给OsExit的副本,并在测试过程中分配了另一个函数。
func TestExitIfNotZero(t *testing.T) {
oldOsExit := OsExit // 変数内の値(os.Exit関数)をコピー
defer func() { OsExit = oldOsExit }() // テスト終了後に元に戻しておく
var capture int // OsExit に到達したかチェックするための変数を用意
OsExit = func(code int) { capture = code } // テスト用のダミー関数をモックに代入
testStatus := 10 // テスト用の引数
ExitIfNotZero(testStatus) // テストの実行
// Assert equal
expect := testStatus // 10
actual := capture
if expect != actual {
t.Errorf("Fail assert equal. Expect: %v Actual: %v", expect, actual)
}
}
オンラインで動作をみる @ Go Playground
文献引用
Go言語のエラーハンドリングについて @ Qiita
例外(exception)がない理由は? | FAQ @ Golang.jp
testing – 如何在Go中测试os.exit场景 @ coder.work (这是非常有用的信息。非常感谢你。)
“Testing Techniques” P.23 by Andrew Gerrand @ Google I/O 2014