展示功能测试的覆盖范围,无盲点。

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

showing coverage of functional tests without blind spots

问题

我有一个用Go编写的生产代码和功能测试,但功能测试不是用Go编写的。功能测试运行编译后的二进制文件。这是我生产代码的简化版本:main.go

package main

import (
    "fmt"
    "math/rand"
    "os"
    "time"
)

func main() {
    rand.Seed(time.Now().UTC().UnixNano())
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            os.Exit(0)
        }
        if i%2 == 0 {
            os.Exit(1)
        }
        time.Sleep(time.Second)
    }
}

我想为我的功能测试构建覆盖率文件。为了做到这一点,我添加了一个名为main_test.go的文件,内容如下:

package main

import (
    "os"
    "testing"
)

var exitCode int

func Test_main(t *testing.T) {
    go main()
    exitCode = <-exitCh
}

func TestMain(m *testing.M) {
    m.Run()
    // 可以退出,因为覆盖率文件已经写入
    os.Exit(exitCode)
}

并修改了main.go

package main

import (
    "flag"
    "fmt"
    "math/rand"
    "os"
    "runtime"
    "time"
)

var exitCh chan int = make(chan int)

func main() {
    rand.Seed(time.Now().UTC().UnixNano())
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            exit(0)
        }
        if i%2 == 0 {
            fmt.Println("status 1")
            exit(1)
        }
        time.Sleep(time.Second)
    }
}

func exit(code int) {
    if flag.Lookup("test.coverprofile") != nil {
        exitCh <- code
        runtime.Goexit()
    } else {
        os.Exit(code)
    }
}

然后我构建覆盖率二进制文件:

go test -c -coverpkg=. -o myProgram

然后我的功能测试运行这个覆盖率二进制文件,像这样:

./myProgram -test.coverprofile=/tmp/profile
6507374435908599516
PASS
coverage: 64.3% of statements in .

然后我构建显示覆盖率的HTML输出:

go tool cover -html /tmp/profile -o /tmp/profile.html
open /tmp/profile.html

问题是,由于条件if flag.Lookup("test.coverprofile") != nil,方法exit永远不会显示100%的覆盖率。因此,os.Exit(code)这一行在我的覆盖率结果中是一个盲点,尽管实际上,功能测试会执行这一行,这一行应该显示为绿色。

另一方面,如果我删除条件if flag.Lookup("test.coverprofile") != nil,那么os.Exit(code)这一行将终止我的二进制文件而不会构建覆盖率文件。

如何重写exit()和可能的main_test.go以显示没有盲点的覆盖率呢?

脑海中首先浮现的解决方案是使用time.Sleep()

func exit(code int) {
    exitCh <- code
    time.Sleep(time.Second) // 等待一段时间以确保覆盖率文件被写入
    os.Exit(code)
}

但这不是很好,因为会导致生产代码在退出之前变慢。

英文:

I have a production golang code and functional tests for it written not in golang. Functional tests run compiled binary. Very simplified version of my production code is here: main.go:

package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;os&quot;
    &quot;time&quot;
)

func main() {
    rand.Seed(time.Now().UTC().UnixNano())
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            os.Exit(0)
        }
        if i%2 == 0 {
            os.Exit(1)
        }
        time.Sleep(time.Second)
    }
}

I want to build coverage profile for my functional tests. In order to do it I add main_test.go file with content:

package main

import (
    &quot;os&quot;
    &quot;testing&quot;
)

var exitCode int

func Test_main(t *testing.T) {
    go main()
    exitCode = &lt;-exitCh
}

func TestMain(m *testing.M) {
    m.Run()
    // can exit because cover profile is already written
    os.Exit(exitCode)
}

And modify main.go:

package main

import (
    &quot;flag&quot;
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;os&quot;
    &quot;runtime&quot;
    &quot;time&quot;
)

var exitCh chan int = make(chan int)

func main() {
    rand.Seed(time.Now().UTC().UnixNano())
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            exit(0)
        }
        if i%2 == 0 {
            fmt.Println(&quot;status 1&quot;)
            exit(1)
        }
        time.Sleep(time.Second)
    }
}

func exit(code int) {
    if flag.Lookup(&quot;test.coverprofile&quot;) != nil {
        exitCh &lt;- code
        runtime.Goexit()
    } else {
        os.Exit(code)
    }
}

Then I build coverage binary:

go test -c -coverpkg=.  -o myProgram

Then my functional tests run this coverage binary, like this:

./myProgram -test.coverprofile=/tmp/profile
6507374435908599516
PASS
coverage: 64.3% of statements in .

And I build HTML output showing coverage:

$ go tool cover -html /tmp/profile -o /tmp/profile.html
$ open /tmp/profile.html

展示功能测试的覆盖范围,无盲点。

Problem

Method exit will never show 100% coverage because of condition if flag.Lookup(&quot;test.coverprofile&quot;) != nil. So line os.Exit(code) is kinda blind spot for my coverage results, although, in fact, functional tests go on this line and this line should be shown as green.

On the other hand, if I remove condition if flag.Lookup(&quot;test.coverprofile&quot;) != nil, the line os.Exit(code) will terminate my binary without building coverage profile.

How to rewrite exit() and maybe main_test.go to show coverage without blind spots?

The first solution that comes into mind is time.Sleep():

func exit(code int) {
        exitCh &lt;- code
        time.Sleep(time.Second) // wait some time to let coverprofile be written
        os.Exit(code)
    }
}

But it's not very good because will cause production code slow down before exit.

答案1

得分: 2

根据我们在评论中的对话,我们的覆盖率配置文件永远不会包含那行代码,因为它永远不会被执行。

在没有看到你的完整代码之前,很难提出一个合适的解决方案,但有几件事情可以做,以提高覆盖率而不会牺牲太多。

主函数和TestMain函数

GOLANG中,通常的做法是避免测试主应用程序入口点,因此大多数专业人士会将尽可能多的功能提取到其他类中,以便于测试。

GOLANG的测试框架允许您在没有主函数的情况下测试应用程序,而是使用TestMain函数来测试需要在主线程上运行的代码。下面是来自GOLANG Testing的一个小片段。

有时测试程序需要在测试之前或之后进行额外的设置或拆卸。有时测试需要控制在main线程上运行的代码。为了支持这些和其他情况,如果一个测试文件包含一个函数:func TestMain(m *testing.M)

请查看GOLANG Testing以获取更多信息。

工作示例

下面是一个示例(覆盖率为93.3%,我们将使其达到100%),测试了代码的所有功能。我对你的设计进行了一些更改,因为它不太适合测试,但功能仍然相同。

package main

// dofunc.go

import (
    "fmt"
    "math/rand"
    "time"
)

var seed int64 = time.Now().UTC().UnixNano()

func doFunc() int {
    rand.Seed(seed)
    var code int
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            code = 0
            break
        }
        if i%2 == 0 {
            fmt.Println("status 1")
            code = 1
            break
        }
        time.Sleep(time.Second)
    }
    return code
}
// dofunc_test.go

package main

import (
    "testing"
    "flag"
    "os"
)

var exitCode int

func TestMain(m *testing.M) {
    flag.Parse()
    code := m.Run()
    os.Exit(code)
}

func TestDoFuncErrorCodeZero(t *testing.T) {
    seed = 2

    if code := doFunc(); code != 0 {
        t.Fail()
    }
}

func TestDoFuncErrorCodeOne(t *testing.T) {
    seed = 3

    if code := doFunc(); code != 1 {
        t.Fail()
    }
}
// main.go

package main

import "os"

func main() {
    os.Exit(doFunc())
}

运行测试

如果我们使用覆盖率配置文件构建应用程序。

$ go test -c -coverpkg=. -o example

然后运行它。

$ ./example -test.coverprofile=/tmp/profile

运行测试:

1543039099823358511
2444694468985893231
6640668014774057861
6019456696934794384
status 1
PASS
coverage: 93.3% of statements in .

所以我们可以看到我们得到了93%的覆盖率,我们知道这是因为我们没有对main进行任何测试覆盖,为了修复这个问题,我们可以编写一些针对它的测试(不是一个很好的主意),因为代码中有os.Exit,或者我们可以对其进行重构,使其非常简单,功能非常少,可以将其排除在我们的测试之外。

为了从覆盖率报告中排除main.go文件,我们可以使用构建标签,在main.go文件的第一行放置标签注释。

//+build !test

有关构建标记的更多信息,请查看此链接:http://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool

这将告诉GOLANG在存在标签构建的地方包含该文件在构建过程中,但在存在标签测试的地方不包含该文件。

查看完整代码。

//+build !test

package main

import "os"

func main() {
    os.Exit(doFunc())
}

我们需要稍微不同地构建覆盖应用程序。

$ go test -c -coverpkg=. -o example -tags test

运行它将是相同的。

$ ./example -test.coverprofile=/tmp/profile

我们得到以下报告。

1543039099823358511
2444694468985893231
6640668014774057861
6019456696934794384
status 1
PASS
coverage: 100.0% of statements in .

现在我们可以构建覆盖率的HTML报告。

$ go tool cover -html /tmp/profile -o /tmp/profile.html

展示功能测试的覆盖范围,无盲点。

英文:

As per our conversation in the comments our coverage profile will never include that line of code because it will never be executed.

With out seeing your full code it is hard to come up with a proper solutions however there a few things you can do to increase the coverage with out sacrificing too much.

func Main and TestMain

Standard practice for GOLANG is to avoid testing the main application entry point so most professionals extract as much functionality into other classes so they can be easily tested.

GOLANG testing framework allows you to test your application with out the main function but in it place you can use the TestMain func which can be used to test where the code needs to be run on the main thread. Below is a small exert from GOLANG Testing.

> It is sometimes necessary for a test program to do extra setup or teardown before or after testing. It is also sometimes necessary for a test to control which code runs on the main thread. To support these and other cases, if a test file contains a function: func TestMain(m *testing.M)

Check GOLANG Testing for more information.

Working example

Below is an example (with 93.3% coverage that we will make it 100%) that tests all the functionality of your code. I made a few changes to your design because it did not lend itself very well for testing but the functionality still the same.

package main

> dofunc.go

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;
)

var seed int64 = time.Now().UTC().UnixNano()

func doFunc() int {
    rand.Seed(seed)
    var code int
    for {
        i := rand.Int()
        fmt.Println(i)
        if i%3 == 0 {
            code = 0
            break
        }
        if i%2 == 0 {
            fmt.Println(&quot;status 1&quot;)
            code = 1
            break
        }
        time.Sleep(time.Second)
    }
    return code
}

> dofunc_test.go

package main

import (
    &quot;testing&quot;
    &quot;flag&quot;
    &quot;os&quot;
)

var exitCode int

func TestMain(m *testing.M) {
	flag.Parse()
	code := m.Run()
	os.Exit(code)
}

func TestDoFuncErrorCodeZero(t *testing.T) {
    seed = 2
    
    if code:= doFunc(); code != 0 {
    	t.Fail()
    }
}

func TestDoFuncErrorCodeOne(t *testing.T) {
    seed = 3
    
    if code:= doFunc(); code != 1 {
    	t.Fail()
    }
}

> main.go

package main

import &quot;os&quot;

func main() {
    os.Exit(doFunc());
}

Running the tests

If we build our application with the cover profile.

> $ go test -c -coverpkg=. -o example

And run it.

> $ ./example -test.coverprofile=/tmp/profile

Running the tests

1543039099823358511
2444694468985893231
6640668014774057861
6019456696934794384
status 1
PASS
coverage: 93.3% of statements in .

So we see that we got 93% coverage we know that is because we don't have any test coverage for main to fix this we could write some tests for it (not a very good idea) since the code has os.Exit or we can refactor it so it is super simple with very little functionality we can exclude it from our tests.

To exclude the main.go file from the coverage reports we can use build tags by placing tag comment at the first line of the main.go file.

> //+build !test

For more information about build flags check this link: http://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool

This will tell GOLANG that the file should be included in the build process where the tag build is present butNOT where the tag test is present.

See full code.

//+build !test

package main

import &quot;os&quot;

func main() {
    os.Exit(doFunc());
}

We need need to build the the coverage application slightly different.

> $ go test -c -coverpkg=. -o example -tags test

Running it would be the same.

> $ ./example -test.coverprofile=/tmp/profile

We get the report below.

1543039099823358511
2444694468985893231
6640668014774057861
6019456696934794384
status 1
PASS
coverage: 100.0% of statements in .

We can now build the coverage html out.

> $ go tool cover -html /tmp/profile -o /tmp/profile.html

展示功能测试的覆盖范围,无盲点。

答案2

得分: 0

在我的pkglint项目中,我声明了一个包可见的变量:

var exit = os.Exit

在设置测试的代码中,我用一个特定于测试的函数覆盖它,当拆除测试时,我将其重置为os.Exit。

这是一个简单而实用的解决方案,对我来说效果很好,至少在一年的广泛测试中。我获得了100%的分支覆盖率,因为根本没有涉及到分支。

英文:

In my pkglint project I have declared a package-visible variable:

var exit = os.Exit

In the code that sets up the test, I overwrite it with a test-specific function, and when tearing down a test, I reset it back to os.Exit.

This is a simple and pragmatic solution that works well for me, for at least a year of extensive testing. I get 100% branch coverage because there is no branch involved at all.

huangapple
  • 本文由 发表于 2016年9月26日 02:44:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/39690509.html
匿名

发表评论

匿名网友

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

确定