一些 Go test 技巧

栏目: IT技术 · 发布时间: 4年前

内容简介:测试驱动开发是一个写出高质量代码的好方法,同时避免将代码越写越烂,并证明你的代码能实现预期的效果。在Go中,除了

前言

测试驱动开发是一个写出高质量代码的好方法,同时避免将代码越写越烂,并证明你的代码能实现预期的效果。

以Table Driven的方式写测试用例

Table Driven 的意思就是以表格的形式写好测试用例的输入和期望结果,然后写完所有测试用例之后直接在一个循环里遍历所有测试用例,这样的好处是你只需要专注写测试用例的输入和期望结果就OK了。

package add

import "testing"

func TestAdd(t *testing.T) {
    tests := [][]int{ // test cases table
        {1, 1, 2},
        {100, 200, 300},
        {-2, 2, 0},
        {-3, -5, -8},
        {999, -1, 998},
    }

    for _, tc := range tests { // 遍历所有测试用例
        if res := Add(tc[0], tc[1]); res != tc[2] {
            t.Errorf("want: %d, got: %d", tc[2], res)
        }
    }
}

外部测试放在 "_test" 包

Go 中,除了 _test.go 文件,同一个目录下的文件都属于同一个包。将测试用例代码放在不同的包里,那么你写测试用例代码时,就好像这个包的真实的使用者一样。在同一个包内调用或写测试用例可能你觉得没什么问题,但当你需要暴露API给外部调用的时候,就考验你的代码功力了,因为你的任何改动都会影响到使用者。

一些 Go test 技巧

这样,在你更改内部的实现时也不需要更改测试用例的代码了。

内部测试放在 *_internal_test.go 文件

如果你需要写一些内部调用的测试用例,那么你可以将文件命名为 _internal_test.go 后缀,然后使用相同的package。内部测试要比API接口更加精细,但这是一个能使你的代码更可靠的好方法,尤其适合测试驱动开发。

一些 Go test 技巧

所以,一个完整的测试代码目录结构是这样的: 一些 Go test 技巧

利用interface写出可测试代码

现在假设我们正在实现一个叫 web 的web操作公共库,这个库提供了 Client 对象和 GetData 方法,用于从web服务中取数据,代码如下:

package web

type Client struct{}

func NewClient() Client {
	return Client{}
}

func (c Client) GetData() (string, error) {
	return "data", nil
}

接着我们的 foo 包会引用 web 包的 Client 对象 GetData 方法去web服务中取数据,代码如下:

package foo

import (
	"errors"

	"interfaces/web"
)

func Controller() error {
	webClient := web.NewClient()
	fromWebAPI, err := webClient.GetData()
	if err != nil {
		return err
	}
	// do some things based on data from web API
	if fromWebAPI != "data" {
		return errors.New("unexpected data")
	}
	return nil
}

现在我们需要测试Controller方法并分别写了两个测试方法,一个是测试成功获取到数据,另一个是测试两种获取数据失败的情况。

然后,问题来了。我们似乎没有办法同时测试到这些逻辑分支,因为我们没办法改变 web 包里面的逻辑。

package foo_test

import (
	"testing"

	"interfaces/foo"
)

func TestController_Success(t *testing.T) {
	err := foo.Controller()
	if err != nil {
		t.FailNow()
	}
}

func TestController_Failure(t *testing.T) {
    // 这里我们想返回错误,但似乎比较难。
	err := foo.Controller()
	if err == nil {
        // 这个测试将会fail :(
		t.FailNow()
	}
}

到这里似乎把我们难住了,但如果我们将 web 包中的 Client 定义成interface,那我们就可以很容易的替换掉这个 Client 的实现。例如,改成下面这样:

package foo

import (
	"errors"
)

type IWebClient interface {
	GetData() (string, error)
}

func Controller(webClient IWebClient) error {
	fromWebAPI, err := webClient.GetData()
	if err != nil {
		return err
	}
	// do some things based on data from web API
	if fromWebAPI != "data" {
		return errors.New("unexpected data")
	}
	return nil
}

然后,我们就可以很容易的测试 Controller 方法了,我们可以根据需要mock Client的实现。

package foo_test

import (
	"errors"
	"testing"

	"interfaces/foo"
)

type MockClient struct {
	GetDataReturn string
}

func (mc MockClient) GetData() (string, error) {
	return mc.GetDataReturn, nil
}

func TestController_Success(t *testing.T) {
	err := foo.Controller(MockClient{"data"})
	if err != nil {
		t.FailNow()
	}
}

type FailingClient struct{}

func (fc FailingClient) GetData() (string, error) {
	return "", errors.New("oh no")
}

func TestController_Failure(t *testing.T) {
	// GetData() 失败分支
	err := foo.Controller(FailingClient{})
	if err == nil {
		t.FailNow()
	}
    // 错误数据分支
	err = foo.Controller(MockClient{"not data"})
	if err == nil {
		t.FailNow()
	}
}

就这样,我们所有代码的分支都已经覆盖到啦~

Test Fixtures, Golden Files

在一些场景下,我们需要读取某些资源文件,比如我们在测试对 json 文件的解码功能时,就需要一些示例的 json 文件作为测试 case 的输入。像这种场景,把这些测试过程中用到的辅助文件,通常就叫做 Test Fixtures 。而放置这些文件的最佳位置就是放在叫 testdata 的目录下,主要原因有两条:

testdata

看下面的例子:

func helperLoadBytes(t *testing.T, name string) []byte {
    bytes, err := ioutil.ReadFile("testdata/somefixture.json")
    if err != nil {
        t.Fatal(err)
    }
    return bytes
}

那么, Golden Files 又是什么呢?Golden Files 其实就是 Test Fixtures 中的一种,当测试用例的输出结果比较简单的时候,我们还可以把输出结果写在测试代码中。

但是当输出结果比较复杂时,直接写入代码已经不太合适了。所以此时,我们通常会把正确结果写入到文件里面,并且测试代码运行时,需要读取这个文件的内容进行比较。

一般 Golden Files 的使用都会配合 Table Driven ,每一个测试 case 的 Golden Files 的名字一般就会以“case名字+.golden” 来命名,这样在编写代码时也会比较简单。

计算测试覆盖率

测试覆盖率表示测试代码覆盖源代码的比例。

Go 1.2引入了一个新的计算test coverage的方法,其原理很简单:在编译之前,重写包的源代码和加入埋点,然后编译和运行重写后的代码,然后根据埋点就能统计出代码的覆盖率了。这个重写其实很简单,因为从测试到运行都是由go的原生 工具 链控制的。

示例代码:

package size

func Size(a int) string {
    switch {
    case a < 0:
        return "negative"
    case a == 0:
        return "zero"
    case a < 10:
        return "small"
    case a < 100:
        return "big"
    case a < 1000:
        return "huge"
    }
    return "enormous"
}

测试代码:

package size

import "testing"

type Test struct {
    in  int
    out string
}

var tests = []Test{
    {-1, "negative"},
    {5, "small"},
}

func TestSize(t *testing.T) {
    for i, test := range tests {
        size := Size(test.in)
        if size != test.out {
            t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
        }
    }
}

然后当我们加上 -cover 参数即 go test -cover ,就可以得出测试覆盖率。

% go test -cover
PASS
coverage: 42.9% of statements
ok  	size	0.026s
%

我们再来看看go test内部是怎么加埋点的,下面是编译前埋点后的伪代码:

func Size(a int) string {
    GoCover.Count[0] = 1 // 埋点1
    switch {
    case a < 0:
        GoCover.Count[2] = 1 // 埋点2...
        return "negative"
    case a == 0:
        GoCover.Count[3] = 1
        return "zero"
    case a < 10:
        GoCover.Count[4] = 1
        return "small"
    case a < 100:
        GoCover.Count[5] = 1
        return "big"
    case a < 1000:
        GoCover.Count[6] = 1
        return "huge"
    }
    GoCover.Count[1] = 1
    return "enormous"
}

其实就是在代码的各个分支加上埋点,最好再计算出覆盖率。

另外,go还提供了一种直观炫酷的方式展示测试的覆盖率,能精确到是否覆盖到某一行代码。执行以下命令:

go test -coverprofile=coverage.out && go tool cover -html=coverage.out

然后会自动的在浏览器中打开页面,如下:

一些 Go test 技巧

总结

单元测试是保证代码质量十分重要的一个环节。Go的源码和著名开源库,都是一边写源码一边写单元测试。在选择开源库的时候,测试覆盖率及测试用例的质量可以作为一个重要的指标。

最后在贴下 Dave Cheney 在推特转发的一些关于测试的哲学,十分有意思且有道理。

一些 Go test 技巧

参考资料

  • 《Go 项目单元测试最佳实践》:https://fatsheep9146.github.io/2018/08/19/Go-%E9%A1%B9%E7%9B%AE%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/
  • 《5 simple tips and tricks for writing unit tests in #golang》:https://medium.com/@matryer/5-simple-tips-and-tricks-for-writing-unit-tests-in-golang-619653f90742
  • 《The cover story》:https://blog.golang.org/cover
  • 《Using Go Interfaces for Testable Code》:https://medium.com/swlh/using-go-interfaces-for-testable-code-d2e11b02dea

一些 Go test 技巧


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python学习手册(第4版)

Python学习手册(第4版)

[美] Mark Lutz / 李军、刘红伟 / 机械工业出版社 / 2011-4 / 119.00元

Google和YouTube由于Python的高可适应性、易于维护以及适合于快速开发而采用它。如果你想要编写高质量、高效的并且易于与其他语言和工具集成的代码,《Python学习手册:第4 版》将帮助你使用Python快速实现这一点,不管你是编程新手还是Python初学者。本书是易于掌握和自学的教程,根据作者Python专家Mark Lutz的著名培训课程编写而成。 《Python学习手册:第......一起来看看 《Python学习手册(第4版)》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器