使用覆盖率信息测试Go中的os.Exit场景(coveralls.io/Goveralls)

时间:2016-11-15 17:03:52

标签: unit-testing testing go architecture coveralls

这个问题: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)
}

然而,这种方法似乎受到某些限制:

  1. 使用goveralls / coveralls.io进行覆盖测试不起作用 - 例如参见示例here(与上面相同的代码,但为方便起见放入github),这会产生覆盖率测试{{3 ,即它不记录正在运行的测试函数。 请注意,您不需要这些链接来回答问题 - 上面的示例将正常工作 - 它们只是用于显示如果将上述内容放入github会发生什么,并将其一直带到travis中coveralls.io

  2. 重新运行测试二进制文件似乎很脆弱。

  3. 具体而言,根据要求,这是覆盖失败的屏幕截图(而不是链接);红色阴影表示就coveralls.io而言,Crasher()未被调用。

    here

    有解决方法吗?特别是第一点。

    在golang级别,问题是:

    • Goveralls框架运行go test -cover ...,它会调用上面的测试。

    • 以上测试在操作系统参数中调用exec.Command / .Run而不是-cover

    • 无条件地将-cover等放在参数列表中是没有吸引力的,因为它将在非覆盖测试中运行覆盖测试(作为子进程),并解析参数列表中是否存在-cover等似乎是一个重要的解决方案。

    • 即使我把-cover等放在参数列表中,我的理解是我会将两个覆盖输出写入同一个文件,这不会起作用 - 这些会需要以某种方式合并。我最接近的是Coverage test showing Crasher() not being called

    摘要

    我所追求的是一种简单的方法来进行覆盖测试(最好通过travis,goveralls和coveralls.io),在这两种情况下,测试例程可以用OS.exit()进行测试,并且注意到该测试的覆盖范围。我非常喜欢使用上面的re-exec方法(如果可以使用),如果可以使它工作。

    解决方案应显示Crasher()的覆盖率测试。从覆盖测试中排除Crasher()不是一种选择,因为在现实世界中我要做的是测试一个更复杂的功能,在某些情况下,在某些条件下,它会调用例如log.Fatalf();我所覆盖的测试是对这些条件的测试是否正常。

3 个答案:

答案 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函数进行测试并不常见。有一个问题已经回答了同样的问题。

  

showing coverage of functional tests without blind spots

总结

总结一下,你应该避免在应用程序的主入口点附近进行测试,并尝试以Main函数上的小代码的方式设计应用程序,这样它就能解耦到足以让你进行多少测试你的代码尽可能。

查看GOLANG Testing了解详情。

覆盖率达到100%

我详细介绍了之前的答案,因为尝试围绕Main函数进行测试是一个坏主意,最佳做法是尽可能少地编写代码,以便可以在盲目的情况下正确测试如果试图在尝试包含Main func时尝试获得100%的覆盖率是值得的,那么最好在测试中忽略它。

您可以使用构建代码从测试中排除main.go文件,从而达到 100%覆盖率或全部绿色

检查:showing coverage of functional tests without blind spots

如果您很好地设计代码并保持所有实际功能的良好分离和测试,只需要几行代码,然后调用实际完成所有工作的实际代码并经过充分测试即可。真的很重要的是你没有测试一个微小且不重要的代码。