【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)”」,却没有找到恰好的文章,所以只是为了测试一下我的谷歌搜索能力。

总结一下(今北产业)

    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() 来结束的测试案例,也就是测试自身的测试。

辅助函数是指用于测试的辅助函数。换句话说,它指的是仅在测试内(***_test.go文件内)定义的测试中使用的用户函数。

那么,我想在另外一个进程中执行测试来分开测试。

在另一个进程中执行测试。

简单示例

以下是一个测试样例,用于测试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