英文:
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)
}
英文:
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:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论