这个问题:How to test os.exit scenarios in Go(以及其中最高的投票答案)阐述了如何在go内测试os.Exit()
场景。由于os.Exit()
不容易被截获,因此使用的方法是重新调用二进制文件并检查退出值。这个方法由Andrew Gerrand(Go团队的核心成员之一)在slide 23 on this presentation描述;代码非常简单,下面将全文转载。
相关的测试和主要文件看起来像这样(请注意,这对文件本身就是一个MVCE):
package foo
import (
"os"
"os/exec"
"testing"
)
func TestCrasher(t *testing.T) {
if os.Getenv("BE_CRASHER") == "1" {
Crasher() // This causes os.Exit(1) to be called
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() {
fmt.Printf("Error is %v\n", e)
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}
和
package foo
import (
"fmt"
"os"
)
// Coverage testing thinks (incorrectly) that the func below is
// never being called
func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}
然而,这种方法似乎受到某些限制:
使用goveralls / coveralls.io进行覆盖测试不起作用 - 例如参见示例here(与上面相同的代码,但为方便起见放入github),这会产生覆盖率测试{{3 ,即它不记录正在运行的测试函数。 请注意,您不需要这些链接来回答问题 - 上面的示例将正常工作 - 它们只是用于显示如果将上述内容放入github会发生什么,并将其一直带到travis中coveralls.io
重新运行测试二进制文件似乎很脆弱。
具体而言,根据要求,这是覆盖失败的屏幕截图(而不是链接);红色阴影表示就coveralls.io而言,Crasher()
未被调用。
有解决方法吗?特别是第一点。
在golang级别,问题是:
Goveralls框架运行go test -cover ...
,它会调用上面的测试。
以上测试在操作系统参数中调用exec.Command / .Run
而不是-cover
无条件地将-cover
等放在参数列表中是没有吸引力的,因为它将在非覆盖测试中运行覆盖测试(作为子进程),并解析参数列表中是否存在-cover
等似乎是一个重要的解决方案。
即使我把-cover
等放在参数列表中,我的理解是我会将两个覆盖输出写入同一个文件,这不会起作用 - 这些会需要以某种方式合并。我最接近的是。
摘要
我所追求的是一种简单的方法来进行覆盖测试(最好通过travis,goveralls和coveralls.io),在这两种情况下,测试例程可以用OS.exit()
进行测试,并且注意到该测试的覆盖范围。我非常喜欢使用上面的re-exec方法(如果可以使用),如果可以使它工作。
解决方案应显示Crasher()
的覆盖率测试。从覆盖测试中排除Crasher()
不是一种选择,因为在现实世界中我要做的是测试一个更复杂的功能,在某些情况下,在某些条件下,它会调用例如log.Fatalf()
;我所覆盖的测试是对这些条件的测试是否正常。
答案 0 :(得分:12)
通过轻微的重构,您可以轻松实现100%的覆盖率。
foo/bar.go
:
package foo
import (
"fmt"
"os"
)
var osExit = os.Exit
func Crasher() {
fmt.Println("Going down in flames!")
osExit(1)
}
测试代码:foo/bar_test.go
:
package foo
import "testing"
func TestCrasher(t *testing.T) {
// Save current function and restore at the end:
oldOsExit := osExit
defer func() { osExit = oldOsExit }()
var got int
myExit := func(code int) {
got = code
}
osExit = myExit
Crasher()
if exp := 1; got != exp {
t.Errorf("Expected exit code: %d, got: %d", exp, got)
}
}
正在运行go test -cover
:
Going down in flames!
PASS
coverage: 100.0% of statements
ok foo 0.002s
是的,您可能会说如果明确调用os.Exit()
,这会有效,但如果其他人调用os.Exit()
该怎么办,例如log.Fatalf()
?
同样的技术也适用于此,您只需切换log.Fatalf()
而不是os.Exit()
,例如:
foo/bar.go
的相关部分:
var logFatalf = log.Fatalf
func Crasher() {
fmt.Println("Going down in flames!")
logFatalf("Exiting with code: %d", 1)
}
测试代码:TestCrasher()
中的foo/bar_test.go
:
func TestCrasher(t *testing.T) {
// Save current function and restore at the end:
oldLogFatalf := logFatalf
defer func() { logFatalf = oldLogFatalf }()
var gotFormat string
var gotV []interface{}
myFatalf := func(format string, v ...interface{}) {
gotFormat, gotV = format, v
}
logFatalf = myFatalf
Crasher()
expFormat, expV := "Exiting with code: %d", []interface{}{1}
if gotFormat != expFormat || !reflect.DeepEqual(gotV, expV) {
t.Error("Something went wrong")
}
}
正在运行go test -cover
:
Going down in flames!
PASS
coverage: 100.0% of statements
ok foo 0.002s
答案 1 :(得分:5)
使用Go接口可以创建可模拟的合成。类型可以将接口作为绑定依赖项。这些依赖项可以很容易地用适合接口的模拟代替。
type Exiter interface {
Exit(int)
}
type osExit struct {}
func (o* osExit) Exit (code int) {
os.Exit(code)
}
type Crasher struct {
Exiter
}
func (c *Crasher) Crash() {
fmt.Println("Going down in flames!")
c.Exit(1)
}
测试
type MockOsExit struct {
ExitCode int
}
func (m *MockOsExit) Exit(code int){
m.ExitCode = code
}
func TestCrasher(t *testing.T) {
crasher := &Crasher{&MockOsExit{}}
crasher.Crash() // This causes os.Exit(1) to be called
f := crasher.Exiter.(*MockOsExit)
if f.ExitCode == 1 {
fmt.Printf("Error code is %d\n", f.ExitCode)
return
}
t.Fatalf("Process ran with err code %d, want exit status 1", f.ExitCode)
}
<强> 缺点 强>
原始Exit
方法仍然不会被测试,所以它应该只对退出负责,仅此而已。
功能是Go中的一等公民。函数允许很多操作,所以我们可以直接用函数做一些技巧。
使用'pass as parameter'操作,我们可以进行依赖注入:
type osExit func(code int)
func Crasher(os_exit osExit) {
fmt.Println("Going down in flames!")
os_exit(1)
}
测试:
var exit_code int
func os_exit_mock(code int) {
exit_code = code
}
func TestCrasher(t *testing.T) {
Crasher(os_exit_mock) // This causes os.Exit(1) to be called
if exit_code == 1 {
fmt.Printf("Error code is %d\n", exit_code)
return
}
t.Fatalf("Process ran with err code %v, want exit status 1", exit_code)
}
<强> 缺点 强>
您必须将依赖项作为参数传递。如果你有很多依赖项,那么params列表的长度可能很大。
实际上可以使用“赋值给变量”操作来实现,而无需将函数显式传递为参数。
var osExit = os.Exit
func Crasher() {
fmt.Println("Going down in flames!")
osExit(1)
}
测试
var exit_code int
func osExitMock(code int) {
exit_code = code
}
func TestCrasher(t *testing.T) {
origOsExit := osExit
osExit = osExitMock
// Don't forget to switch functions back!
defer func() { osExit = origOsExit }()
Crasher()
if exit_code != 1 {
t.Fatalf("Process ran with err code %v, want exit status 1", exit_code)
}
}
<强> 缺点 强>
隐含且容易崩溃。
如果您计划在Exit
以下声明某个逻辑,退出逻辑必须在退出后与else
块或额外return
隔离,因为mock不会停止执行。
func (c *Crasher) Crash() {
if SomeCondition == true {
fmt.Println("Going down in flames!")
c.Exit(1) // Exit in real situation, invoke mock when testing
} else {
DoSomeOtherStuff()
}
}
答案 2 :(得分:0)
由于类似的问题,在Main
中围绕应用程序的GOLANG
函数进行测试并不常见。有一个问题已经回答了同样的问题。
总结一下,你应该避免在应用程序的主入口点附近进行测试,并尝试以Main
函数上的小代码的方式设计应用程序,这样它就能解耦到足以让你进行多少测试你的代码尽可能。
查看GOLANG Testing了解详情。
我详细介绍了之前的答案,因为尝试围绕Main
函数进行测试是一个坏主意,最佳做法是尽可能少地编写代码,以便可以在盲目的情况下正确测试如果试图在尝试包含Main
func时尝试获得100%的覆盖率是值得的,那么最好在测试中忽略它。
您可以使用构建代码从测试中排除main.go
文件,从而达到 100%覆盖率或全部绿色。
检查:showing coverage of functional tests without blind spots
如果您很好地设计代码并保持所有实际功能的良好分离和测试,只需要几行代码,然后调用实际完成所有工作的实际代码并经过充分测试即可。真的很重要的是你没有测试一个微小且不重要的代码。