使用覆盖信息在 Go 中测试 os.Exit 个场景(coveralls.io/Goveralls)
Testing os.Exit scenarios in Go with coverage information (coveralls.io/Goveralls)
本题:How to test os.exit scenarios in Go (and the highest voted answer therein) sets out how to test os.Exit()
scenarios within go. As os.Exit()
cannot easily be intercepted, the method used is to reinvoke the binary and check the exit value. This method is described at slide 23 on this presentationAndrew Gerrand(Go团队核心成员之一);代码非常简单,完整转载如下。
相关的测试文件和主文件如下所示(注意这对文件本身就是一个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 (the same code as above but put into github for your convenience) which produces the coverage test here,即它不记录测试函数 运行。 请注意,您不需要那些 link 来回答问题 - 上面的示例可以正常工作 - 它们只是用来展示如果将上述内容放入 [=92= 中会发生什么], 并一路通过 travis 到达 coveralls.io
重新运行测试二进制文件显得脆弱。
具体来说,根据要求,这是覆盖失败的屏幕截图(而不是 link);红色阴影表示就 coveralls.io 而言,Crasher()
未被调用。
有办法解决这个问题吗?特别是第一点。
在 golang 级别,问题是这样的:
Goveralls 框架 运行s go test -cover ...
,调用上面的测试。
上面的测试在 OS 参数中调用 exec.Command / .Run
而没有 -cover
无条件地将 -cover
等放入参数列表中是没有吸引力的,因为它会 运行 在非覆盖测试中进行覆盖测试(作为子流程),并且解析参数列表以查找 -cover
等的存在似乎是一个繁重的解决方案。
即使我将 -cover
等放在参数列表中,我的理解是我会将两个覆盖率输出写入同一个文件,这不会工作——这些需要以某种方式合并。我最接近的是 this golang issue.
总结
我所追求的是 运行 进行覆盖率测试的简单方法(最好通过 travis、goveralls 和 coveralls.io),其中可以在两个测试例程退出的情况下进行测试使用 OS.exit()
,并注明该测试的覆盖范围。如果可以的话,我非常希望它使用上面的 re-exec 方法(如果可以的话)。
解决方案应显示 Crasher()
的覆盖率测试。从覆盖测试中排除 Crasher()
不是一种选择,因为在现实世界中,我试图做的是测试一个更复杂的函数,在某个深处,在某些条件下,它调用例如log.Fatalf()
;我进行的覆盖测试是针对这些条件的测试是否正常工作。
在 GOLANG
中围绕应用程序的 Main
功能进行测试并不是常见的做法,特别是因为这样的问题。有一个已经回答的问题触及了同样的问题。
总结
总结一下,您应该避免在应用程序的主要入口点周围进行测试,并尝试以 Main
函数上的代码很少的方式设计您的应用程序,这样它就足够解耦以允许您进行测试尽可能多的代码。
查看 GOLANG Testing 了解更多信息。
覆盖率达到 100%
正如我在上一个答案中详细说明的那样,尝试围绕 Main
函数进行测试是个坏主意,最佳做法是尽可能少地放置代码,以便可以使用出于盲点,在尝试包含 Main
函数的同时尝试获得 100% 的覆盖率是徒劳的,因此最好在测试中忽略它。
您可以使用构建标签从测试中排除 main.go
文件,从而达到 100% 的覆盖率或 all green.
检查:
如果你设计好你的代码并保持所有实际功能很好地解耦和测试有几行代码做的很少,然后调用实际的代码片段来完成所有实际工作并经过良好测试您是否在测试一个微小且不重要的代码并不重要。
稍加重构,您就可以轻松实现 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()
is called explicitly, but what if os.Exit()
is called by someone else, e.g. 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()
in 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
接口和模拟
可以使用 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
方法还没有测试所以它应该只负责退出,仅此而已。
功能第一class公民
参数依赖
函数首先是 class 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)
}
缺点
您必须将依赖项作为参数传递。如果你有很多依赖项,参数列表的长度可能会很大。
变量替换
实际上可以使用 "assign to variable" 操作来完成它,而无需显式传递函数作为参数。
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()
}
}
本题:How to test os.exit scenarios in Go (and the highest voted answer therein) sets out how to test os.Exit()
scenarios within go. As os.Exit()
cannot easily be intercepted, the method used is to reinvoke the binary and check the exit value. This method is described at slide 23 on this presentationAndrew Gerrand(Go团队核心成员之一);代码非常简单,完整转载如下。
相关的测试文件和主文件如下所示(注意这对文件本身就是一个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 (the same code as above but put into github for your convenience) which produces the coverage test here,即它不记录测试函数 运行。 请注意,您不需要那些 link 来回答问题 - 上面的示例可以正常工作 - 它们只是用来展示如果将上述内容放入 [=92= 中会发生什么], 并一路通过 travis 到达 coveralls.io
重新运行测试二进制文件显得脆弱。
具体来说,根据要求,这是覆盖失败的屏幕截图(而不是 link);红色阴影表示就 coveralls.io 而言,Crasher()
未被调用。
有办法解决这个问题吗?特别是第一点。
在 golang 级别,问题是这样的:
Goveralls 框架 运行s
go test -cover ...
,调用上面的测试。上面的测试在 OS 参数中调用
exec.Command / .Run
而没有-cover
无条件地将
-cover
等放入参数列表中是没有吸引力的,因为它会 运行 在非覆盖测试中进行覆盖测试(作为子流程),并且解析参数列表以查找-cover
等的存在似乎是一个繁重的解决方案。即使我将
-cover
等放在参数列表中,我的理解是我会将两个覆盖率输出写入同一个文件,这不会工作——这些需要以某种方式合并。我最接近的是 this golang issue.
总结
我所追求的是 运行 进行覆盖率测试的简单方法(最好通过 travis、goveralls 和 coveralls.io),其中可以在两个测试例程退出的情况下进行测试使用 OS.exit()
,并注明该测试的覆盖范围。如果可以的话,我非常希望它使用上面的 re-exec 方法(如果可以的话)。
解决方案应显示 Crasher()
的覆盖率测试。从覆盖测试中排除 Crasher()
不是一种选择,因为在现实世界中,我试图做的是测试一个更复杂的函数,在某个深处,在某些条件下,它调用例如log.Fatalf()
;我进行的覆盖测试是针对这些条件的测试是否正常工作。
在 GOLANG
中围绕应用程序的 Main
功能进行测试并不是常见的做法,特别是因为这样的问题。有一个已经回答的问题触及了同样的问题。
总结
总结一下,您应该避免在应用程序的主要入口点周围进行测试,并尝试以 Main
函数上的代码很少的方式设计您的应用程序,这样它就足够解耦以允许您进行测试尽可能多的代码。
查看 GOLANG Testing 了解更多信息。
覆盖率达到 100%
正如我在上一个答案中详细说明的那样,尝试围绕 Main
函数进行测试是个坏主意,最佳做法是尽可能少地放置代码,以便可以使用出于盲点,在尝试包含 Main
函数的同时尝试获得 100% 的覆盖率是徒劳的,因此最好在测试中忽略它。
您可以使用构建标签从测试中排除 main.go
文件,从而达到 100% 的覆盖率或 all green.
检查:
如果你设计好你的代码并保持所有实际功能很好地解耦和测试有几行代码做的很少,然后调用实际的代码片段来完成所有实际工作并经过良好测试您是否在测试一个微小且不重要的代码并不重要。
稍加重构,您就可以轻松实现 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()
is called explicitly, but what if os.Exit()
is called by someone else, e.g. 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()
in 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
接口和模拟
可以使用 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
方法还没有测试所以它应该只负责退出,仅此而已。
功能第一class公民
参数依赖
函数首先是 class 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)
}
缺点
您必须将依赖项作为参数传递。如果你有很多依赖项,参数列表的长度可能会很大。
变量替换
实际上可以使用 "assign to variable" 操作来完成它,而无需显式传递函数作为参数。
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()
}
}