How to test os.exit scenarios in Go

huangapple go评论93阅读模式
英文:

How to test os.exit scenarios in Go

问题

给定这段代码

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

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

英文:

Given this code

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

How do I properly test that calling this function will result in an exit using go test? This needs to occur within a suite of tests, in other words the os.Exit() call cannot impact the other tests and should be trapped.

答案1

得分: 76

有一个由Go团队的核心成员Andrew Gerrand(一个演讲](https://talks.golang.org/2014/testing.slide),他展示了如何进行测试。

给定一个函数(在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,并通过-test.run=TestCrasher参数限制执行范围为TestCrasher测试。它还通过环境变量(BE_CRASHER=1)传递一个标志,第二次调用会检查该标志,如果设置了,则调用被测试的系统,然后立即返回,以防止陷入无限循环。因此,我们回到了原始的调用点,现在可以验证实际的退出代码。

来源:Andrew的演讲的第23张幻灯片。第二张幻灯片包含演讲视频的链接:presentation's video。他在47:09处谈到了子进程测试。

英文:

There's a presentation by Andrew Gerrand (one of the core members of the Go team) where he shows how to do it.

Given a function (in main.go)

package main

import (
    "fmt"
	"os"
)

func Crasher() {
    fmt.Println("Going down in flames!")
    os.Exit(1)
}

here's how you would test it (through 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)
}

What the code does is invoke go test again in a separate process through exec.Command, limiting execution to the TestCrasher test (via the -test.run=TestCrasher switch). It also passes in a flag via an environment variable (BE_CRASHER=1) which the second invocation checks for and, if set, calls the system-under-test, returning immediately afterwards to prevent running into an infinite loop. Thus, we are being dropped back into our original call site and may now validate the actual exit code.

Source: Slide 23 of Andrew's presentation. The second slide contains a link to the presentation's video as well.
He talks about subprocess tests at 47:09

答案2

得分: 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非常强大。但是它也有一些注意事项

英文:

I do this by using 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 is super-powerful when it comes to this sort of work, and for fault injection and other difficult tasks. It does come with some caveats.

答案3

得分: 7

我不认为你可以在不模拟外部测试(使用exec.Command)的情况下测试实际的os.Exit

话虽如此,你可以通过创建一个接口或函数类型,然后在测试中使用一个空操作的实现来实现你的目标:

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

Go Playground

英文:

I don't think you can test the actual os.Exit without simulating testing from the outside (using exec.Command) process.

That said, you might be able to accomplish your goal by creating an interface or function type and then use a noop implementation in your tests:

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

答案4

得分: 3

你无法直接翻译代码,你需要使用exec.Command并测试返回的值。

英文:

You can't, you would have to use exec.Command and test the returned value.

答案5

得分: 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 // 默认值可以省略 :)

    // 准备测试
    my_private_exit_function = func(c int) {
        ok = true
    }
    // 运行函数
    MyAbstractFunctionAndExit(1)
    // 检查
    if ok == false {
        t.Errorf("AbstractFunction() 中出现错误")
    }
    // 如果需要,恢复
    my_private_exit_function = os.Exit
}

请注意,我只翻译了代码部分,不包括注释。

英文:

Code for testing:

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

Testing code:

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
}

答案6

得分: 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) {
    // 准备模拟设置
    fakeExit := func(int) {}

    p := PatchOSExit(t, fakeExit)
    defer p.Unpatch()

    // 调用应用程序代码
    NewSampleApplication()

    // 断言 os.Exit 是否被调用
    if p.Called == false {
        t.Errorf("Expected os.Exit to be called but it was not called")
        return
    }

    // 同样,断言 os.Exit 是否以正确的代码被调用
    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

英文:

To test the os.Exit like scenarios we can use the https://github.com/undefinedlabs/go-mpatch along with the below code. This ensures that your code remains clean as well as readable and maintainable.

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

You can consume the above code as follows:

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

I've also added a link to Playground: https://go.dev/play/p/FA0dcwVDOm7

答案7

得分: 0

在我的代码中,我刚刚使用了以下代码:

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

然后像这样调用它:

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

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

英文:

In my code I've just used

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

then calling it like:

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

This way doomedOrNot can be tested easily.

huangapple
  • 本文由 发表于 2014年10月7日 05:56:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/26225513.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定