如何在 Go 中测试 os.exit 场景

问题描述 投票:0回答:8

鉴于此代码

func doomed() {
  os.Exit(1)
}

如何正确测试调用此函数是否会导致使用

go test
退出?这需要在一组测试中发生,换句话说,
os.Exit()
调用不能影响其他测试,应该被捕获。

testing go exit-code
8个回答
80
投票

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
) 传递一个标志,第二次调用会检查该标志,如果设置了该标志,则调用被测系统,然后立即返回以防止陷入无限循环。因此,我们将返回到原来的调用站点,现在可以验证实际的退出代码。

来源:Andrew 演示文稿的幻灯片 23。第二张幻灯片还包含指向演示视频的链接。 他在 47:09

谈论了子流程测试

10
投票

我通过使用 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 在此类工作、故障注入和其他困难任务方面非常强大。它确实存在有一些警告


7
投票

我认为如果不模拟外部测试(使用

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)
}

3
投票

你不能,你必须使用

exec.Command
并测试返回的值。


2
投票

测试代码:

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
}

0
投票

要测试

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


0
投票

在我刚刚使用的代码中

func doomedOrNot() int {
  if (doomed) {
    return 1
  }
  return 0
}

然后这样称呼它:

if exitCode := doomedOrNot(); exitCode != 0 {
  os.Exit(exitCode)
}

这样

doomedOrNot
就可以轻松测试了。


0
投票

在单元测试中,您通常具有以下结构:

  1. 安排:设置、创建模拟等
  2. Act:调用函数进行测试
  3. 断言:在函数调用后验证输出/条件与预期结果

您可以通过使用

os.exit()
+
panic
并在传统 UT 结构上稍微改变顺序来实现使用
defer
或“预期”
recover
进行单元测试:

  1. 安排
  2. Assert 逻辑包含在
    defer
  3. 行动

示例:

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()
}
© www.soinside.com 2019 - 2024. All rights reserved.