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

bwntbbo3  于 2023-04-27  发布在  Go
关注(0)|答案(7)|浏览(239)

鉴于此代码

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

如何正确测试调用此函数将导致使用go test退出?这需要在一组测试中进行,换句话说,os.Exit()调用不会影响其他测试,应该被捕获。

smdnsysy

smdnsysy1#

Andrew Gerrand(Go团队的核心成员之一)的presentation展示了如何做到这一点。
给定一个函数(在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)
}

该代码所做的是通过exec.Command在单独的进程中再次调用go test,将执行限制为TestCrasher测试(通过-test.run=TestCrasher开关)。它还通过环境变量传入一个标志(BE_CRASHER=1),第二次调用检查该值,如果设置了该值,则调用被测系统,然后立即返回,以防止运行到无限循环中。因此,我们被退回到原始调用站点,现在可以验证实际的退出代码。
来源:Andrew演示文稿的第23张幻灯片。第二张幻灯片也包含presentation's video的链接。他谈到了47:09的子流程测试。

hs1ihplo

hs1ihplo2#

我使用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在处理这类工作、故障注入和其他困难任务时非常强大。

7uhlpewt

7uhlpewt3#

我认为如果不从外部(使用exec.Command)模拟测试过程,就无法测试实际的os.Exit
也就是说,你可以通过创建一个接口或函数类型,然后在测试中使用noop实现来实现你的目标:
Go Playground

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

cedebl8k4#

你不能,你必须使用exec.Command并测试返回值。

hc8w905p

hc8w905p5#

测试代码:

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

d4so4syb6#

为了测试类似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

js81xvg6

js81xvg67#

在我的代码中我刚刚使用了

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

然后把它叫做:

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

这样doomedOrNot就可以很容易地测试。

相关问题