在Go中进行良好的单元测试

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

Good unit testing in Go

问题

我一直认为好的单元测试是独立的测试。所谓的“独立”是指当函数'A'使用了函数'B'时,在测试函数'A'时,我们会对函数'B'进行模拟/存根处理,以防止当函数'B'工作不正确时导致函数'A'失败。

但是当我们检查golang包的源代码时,这个原则并没有得到遵守。

例如,让我们来看看url包中的url.go和url_test.go:

url.go:

func parseQuery(m Values, query string) (err error) {
    for query != "" {
    ...
    key, err1 := QueryUnescape(key)
    ...

url_test.go:

func TestParseQuery(t *testing.T) {
    for i, test := range parseTests {
        form, err := ParseQuery(test.query)
        ...

正如我们所看到的,parseQuery使用了QueryUnescape。在测试中,QueryUnescape没有以任何方式进行存根处理。因此,如果QueryUnescape工作不正确,parseQuery的测试将失败。

因此,包的作者并不总是满足“独立”单元测试的要求。在这种情况下,为什么不关心这个原则的原因是什么?是否有一些规则允许程序员接受这种形式的单元测试?

在使用Python编写独立测试之后,我对于在编写完美测试(这在golang中对代码设计有很大影响)和结果之间的平衡感到有些困惑。

英文:

I always thought that good unit test, are test that are independent. By 'independent' i mean that when function 'A' uses 'B', and we test function 'A', we mock/stub out 'B' in case when 'B' does not work correctly it will not fail 'A'.

But when we check sources of golang packages that principle is not respected.

For example lets check url.go and url_test.go in url packages:

url.go:

func parseQuery(m Values, query string) (err error) {
    for query != "" {
    ...
    key, err1 := QueryUnescape(key)
    ...

url_test.go:

func TestParseQuery(t *testing.T) {
    for i, test := range parseTests {
        form, err := ParseQuery(test.query)
        ...

As we can see parseQuery use QueryUnescape. In test QueryUnescape is not stubbed in any way. So if QueryUnescape will work wrong, parseQuerytest will fail.

So authors of package not always meet requirement of 'independent' unit test. What was the reason to not bother about this principle in this case, is there some rule that allow programmer of accepting this form of unit tests?

After writing independent test in python, I'am little confused about balancing between writing perfect test(which much affects in golang about design of code) and results.

答案1

得分: 5

你的问题实际上是关于定义“单元测试”中“单元”的范围,这可能有些棘手。一个“单元”不一定意味着一个函数或一个结构体。它可能意味着几个函数/结构体一起工作。但是它肯定意味着你不会跨越进程边界(比如使用真实的文件系统、通过网络、访问数据库等)。这些边界通过集成测试来测试,或者使用测试替身(模拟对象、存根、间谍等)进行模拟。

我是GoConvey的作者,它是一个构建在内置的go“testing”包之上的测试工具。GoConvey有一个全面的单元测试套件。在项目核心的测试中,我不会编写调用每个单个的导出和非导出函数的测试。我编写测试时考虑的是公共的API(导出的函数/结构体)。API的实现不是我的单元测试关注的重点,而是测试结果。通过阅读一些测试,你可以看到我的意思。我只调用几个公共的方法,并断言结果是否正确。实际上有很多结构体和函数被调用,但该包的测试覆盖率非常高(目前为92.8%——虽然不如我希望的高,但已经相当不错了)。

在GoConvey的convey包中,单元是整个包及其组件。一开始,我记得测试更加细粒度,这使我能够更快地进行红-绿-重构循环。随着代码的增长,这些范围较小的测试被更广泛的测试所取代,这似乎更合适。因此,测试套件会随着生产代码的发展而演变。如果直接测试每个方法,你将感到很多摩擦和惯性,因为你无法更改任何内容而不破坏一堆测试。

这里还有一个关于如何确定单元测试范围的有趣总结:

http://blog.8thlight.com/uncle-bob/2014/01/27/TheChickenOrTheRoad.html

英文:

Your question is really about defining the scope of a "unit" when "unit testing", which can be tricky. A "unit" does not necessarily mean a function, or a struct. It might mean a few functions/structs working together. But it most certainly means that you will not cross process boundaries (like using the real file system or going over a network, hitting a database, etc...). The boundaries are tested with integration tests or simulated using test doubles (mocks, stubs, spies, etc...).

I'm the author of GoConvey, a testing tool that build on top of the built-in go "testing" package. GoConvey has a comprehensive suite of unit tests. In the tests for the core of the project I don't write tests that invoke every single exported and non-exported function. I wrote the tests with the public-facing API in mind (the exported functions/structs). The implementation of that API is not the concern of my unit tests but the outcome is. You can see what I mean just by reading a few tests. I only call a few public-facing methods and assert that the result is correct. There are actually lots of structs and functions being invoked but the test coverage in that package is very high (currently 92.8%--not as high as I'd like but it's pretty good).

In the case of the convey package within GoConvey the unit is the entire package and its components. In the beginning I remember the tests being more granular, which allowed me to get into the red-green-refactor cycle more quickly. As the code grew, those smaller-scoped tests were superseded by broader tests that seemed more appropriate. So the suite will evolve with the production code. You'll feel a lot of friction and inertia if every single method is tested directly as you won't be able to change anything without breaking a bunch of tests.

Here's another interesting summary about how to discern the scope of a unit test:

http://blog.8thlight.com/uncle-bob/2014/01/27/TheChickenOrTheRoad.html

huangapple
  • 本文由 发表于 2014年2月27日 20:18:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/22068315.html
匿名

发表评论

匿名网友

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

确定