鉴于此代码
func doomed() {
os.Exit(1)
}
如何正确测试调用此函数是否会导致使用
go test
退出?这需要在一组测试中发生,换句话说,os.Exit()
调用不能影响其他测试,应该被捕获。
Andrew Gerrand(Go 团队的核心成员之一)有一个演示,他展示了如何做到这一点。
给定一个函数(在
main.go
中)
package main
import (
"fmt"
"os"
)
func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}
这是您测试它的方法(通过
main_test.go
):
package main
import (
"os"
"os/exec"
"testing"
)
func TestCrasher(t *testing.T) {
if os.Getenv("BE_CRASHER") == "1" {
Crasher()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
cmd.Env = append(os.Environ(), "BE_CRASHER=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}
代码的作用是通过
go test
在单独的进程中再次调用 exec.Command
,将执行限制为 TestCrasher
测试(通过 -test.run=TestCrasher
开关)。它还通过环境变量 (BE_CRASHER=1
) 传递一个标志,第二次调用会检查该标志,如果设置了该标志,则调用被测系统,然后立即返回以防止陷入无限循环。因此,我们将返回到原来的调用站点,现在可以验证实际的退出代码。
谈论了子流程测试
我通过使用 bouk/monkey 来做到这一点:
func TestDoomed(t *testing.T) {
fakeExit := func(int) {
panic("os.Exit called")
}
patch := monkey.Patch(os.Exit, fakeExit)
defer patch.Unpatch()
assert.PanicsWithValue(t, "os.Exit called", doomed, "os.Exit was not called")
}
monkey 在此类工作、故障注入和其他困难任务方面非常强大。它确实存在有一些警告。
我认为如果不模拟外部测试(使用
os.Exit
)过程,您就无法测试实际的exec.Command
。
也就是说,您可以通过创建接口或函数类型然后在测试中使用 noop 实现来实现您的目标:
package main
import "os"
import "fmt"
type exiter func (code int)
func main() {
doExit(func(code int){})
fmt.Println("got here")
doExit(func(code int){ os.Exit(code)})
}
func doExit(exit exiter) {
exit(1)
}
你不能,你必须使用
exec.Command
并测试返回的值。
测试代码:
package main
import "os"
var my_private_exit_function func(code int) = os.Exit
func main() {
MyAbstractFunctionAndExit(1)
}
func MyAbstractFunctionAndExit(exit int) {
my_private_exit_function(exit)
}
测试代码:
package main
import (
"os"
"testing"
)
func TestMyAbstractFunctionAndExit(t *testing.T) {
var ok bool = false // The default value can be omitted :)
// Prepare testing
my_private_exit_function = func(c int) {
ok = true
}
// Run function
MyAbstractFunctionAndExit(1)
// Check
if ok == false {
t.Errorf("Error in AbstractFunction()")
}
// Restore if need
my_private_exit_function = os.Exit
}
要测试
os.Exit
之类的场景,我们可以使用https://github.com/undefinedlabs/go-mpatch以及以下代码。这可以确保您的代码保持干净、可读和可维护。
type PatchedOSExit struct {
Called bool
CalledWith int
patchFunc *mpatch.Patch
}
func PatchOSExit(t *testing.T, mockOSExitImpl func(int)) *PatchedOSExit {
patchedExit := &PatchedOSExit{Called: false}
patchFunc, err := mpatch.PatchMethod(os.Exit, func(code int) {
patchedExit.Called = true
patchedExit.CalledWith = code
mockOSExitImpl(code)
})
if err != nil {
t.Errorf("Failed to patch os.Exit due to an error: %v", err)
return nil
}
patchedExit.patchFunc = patchFunc
return patchedExit
}
func (p *PatchedOSExit) Unpatch() {
_ = p.patchFunc.Unpatch()
}
您可以按如下方式使用上述代码:
func NewSampleApplication() {
os.Exit(101)
}
func Test_NewSampleApplication_OSExit(t *testing.T) {
// Prepare mock setup
fakeExit := func(int) {}
p := PatchOSExit(t, fakeExit)
defer p.Unpatch()
// Call the application code
NewSampleApplication()
// Assert that os.Exit gets called
if p.Called == false {
t.Errorf("Expected os.Exit to be called but it was not called")
return
}
// Also, Assert that os.Exit gets called with the correct code
expectedCalledWith := 101
if p.CalledWith != expectedCalledWith {
t.Errorf("Expected os.Exit to be called with %d but it was called with %d", expectedCalledWith, p.CalledWith)
return
}
}
我还添加了 Playground 的链接:https://go.dev/play/p/FA0dcwVDOm7
在我刚刚使用的代码中
func doomedOrNot() int {
if (doomed) {
return 1
}
return 0
}
然后这样称呼它:
if exitCode := doomedOrNot(); exitCode != 0 {
os.Exit(exitCode)
}
这样
doomedOrNot
就可以轻松测试了。
在单元测试中,您通常具有以下结构:
您可以通过使用
os.exit()
+ panic
并在传统 UT 结构上稍微改变顺序来实现使用 defer
或“预期”recover
进行单元测试:
defer
示例:
func TestWithExitCondition(t *testing.T) {
// Arrange
assert := assert.New(t)
// more arrange/setup logic
// Capture the panic error/os.exit() using recover
defer func() {
if r := recover(); r != nil {
// Assert
// Test passed if a panic/exit occurred (expected behavior)
t.Log("Test passed as expected")
// Assert logic checking, checking mock instances, etc
}
}()
// Act
RunLogicToBeTestedThatExits()
}