Go 语言基础 - 编写单元测试(译文) - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
darluc
V2EX    Go 编程语言

Go 语言基础 - 编写单元测试(译文)

  •  
  •   darluc 2019-03-09 01:29:50 +08:00 3285 次点击
    这是一个创建于 2458 天前的主题,其中的信息可能已经有所发展或是发生改变。

    查看原文

    在上一篇文章 "Grab JSON from an API" 中,我们探索了如何使用 HTTP 客户端以及如何解析 JSON 数据。本篇文章是 Go 语言主题的续篇,讲述如何编写单元测试。

    1. Go 语言中的测试

    Go 语言有一个自带的测试命令 go test ,还有一个标准 testing 测试包,它能够为你提供一个小却完整的测试体验。

    这套标准工具链还包括了基准测试以及基于语句的代码覆盖率测试,类似与 NCover(.Net) 或者 Istanbul(Node.js)。

    1.2 编写测试代码

    和 Go 语言其它方面如格式化、命名规则一样,Go 语言的单元测试也显得个性十足。它的语法刻意规避了使用断言模式,并将值验证和行为检测的工作留给了开发人员。

    这儿有一个例子,我们要对 main 包里的一个方法进行测试。我们已定义了一个名为 Sum 的出口函数,它接收两个整数参数,并将它们相加。

    package main func Sum(x int, y int) int { return x + y } func main() { Sum(5, 5) } 

    我们在另一个单独的文件中编写测试代码。这个测试文件可以在其它的包(目录)中,或者在相同的包中(main)。以下是一个检测相加结果的单元测试:

    package main import "testing" func TestSum(t *testing.T) { total := Sum(5, 5) if total != 10 { t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10) } } 

    Go 语言的测试函数有以下特征:

    • 只有唯一的参数,必须是 t *testing.T 类型
    • 必须以单词 Test 开头,再组合上首字母大写的单词或词组(一般是被测试的方法名称,如 TestValidateClient
    • 调用 t.Error 或者 t.Fail 方法指明测试失败(这里我使用了 t.Errorf 来提供更多的细节)
    • t.Log 可以用来提供一些失败信息以外的调试信息
    • 测试代码文件名必须是 _test 结尾的形式 something_test.go ,例如:addtion_test.go

    如果你在同一个目录下既有代码也有测试代码,那么你就无法使用 go run *.go 的方式执行你的程序了。我一般会使用 go build 编译出可执行程序,再执行它。

    你可能更习惯于使用 Assert 关键字进行验证工作,不过 The Go Programming Language 的作者们对于 Go 的断言方式做了许多很好的辩解。

    当使用断言时:

    • 测试代码往往会让人觉得他们正在使用另一种语言(比如 RSpec/Mocha )
    • 错误输出看起来令人费解 "assert: 0 == 1"
    • 可能会产生大量的调用栈信息
    • 第一个断言失败后,测试代码会终止执行 - 会掩盖其它的失败可能

    有一些类似 RSpec 或者 Assert 的 Go 语言第三方测试库。比如 stretchr/testify

    测试表

    “测试表”的概念是一组测试输入和输出值的映射。这是一个针对 Sum 函数的例子:

    package main import "testing" func TestSum(t *testing.T) { tables := []struct { x int y int n int }{ {1, 1, 2}, {1, 2, 3}, {2, 2, 4}, {5, 2, 7}, } for _, table := range tables { total := Sum(table.x, table.y) if total != table.n { t.Errorf("Sum of (%d+%d) was incorrect, got: %d, want: %d.", table.x, table.y, total, table.n) } } } 

    如果你想要制造一些错误使得测试无法通过,那么将 Sum 函数的返回部分改为 x * y 即可。

    $ go test -v === RUN TestSum --- FAIL: TestSum (0.00s) table_test.go:19: Sum of (1+1) was incorrect, got: 1, want: 2. table_test.go:19: Sum of (1+2) was incorrect, got: 2, want: 3. table_test.go:19: Sum of (5+2) was incorrect, got: 10, want: 7. FAIL exit status 1 FAIL github.com/alexellis/t6 0.013s 

    启动测试

    有两种方式可以用来启动一个包内的测试代码。这些方法对于单元测试和集成测试是相同的。

    1. 在和测试文件相同的目录中:

      go test 

      这会执行包内所有匹配 _test.go 名称的测试代码

      或者

    2. 采用完整的包名

      go test github.com/alexellis/golangbasics1 

    现在你可以执行 Go 语言单元测试了,可以使用 go test -v 获得更详细的输出,你能看到每条测试的 PASS/FAIL 信息,以及所有 t.Log 打印出的额外日志信息。

    单元测试和集成测试的区别在于,单元测试通常独立于外部依赖,不会与网络、磁盘等产生交互。单元测试一般只关注函数的功能。

    1.3 go test 的更多用法

    语句( statement )覆盖率

    go test 工具自带内建的代码语句覆盖率测试功能。想要用之前的代码例子尝试一下,输入以下命令即可:

    $ go test -cover PASS coverage: 50.0% of statements ok github.com/alexellis/golangbasics1 0.009s 

    较高的语句覆盖率比低覆盖率或者零覆盖率要好,不过这样量化也可能会产生误导。我们想保证我们不只是在执行语句,而且我们还验证了代码的行为和输出,而且在不符合逻辑的地方报错。如果你删除了之前例子代码中的 “ if ” 语句,它仍然会保持 50% 的测试覆盖率,却丧失了验证 “ Sum ” 方法行为的用处。

    生成 HTML 格式的覆盖率测试报告

    如果你使用接下来的两条命令,你就可以直观地看到你的程序哪些部分被覆盖到了,而哪些语句没有被覆盖到:

    go test -cover -coverprofile=c.out go tool cover -html=c.out -o coverage.html 

    然后用浏览器打开 coverage.html 文件。

    Go 编译时不会引入你的测试代码

    还有一点,将 addition_test.go 这样的测试文件留在你的包目录中虽然略有些不自然。不过 Go 语言的编译器和链接器保证不会将你的测试文件编入任何它生成的二进制文件中。

    下面有个例子,可以找出 net/http 包中的生成代码和测试代码。

    $ go list -f={{.GoFiles}} net/http [client.go cookie.go doc.go filetransport.go fs.go h2_bundle.go header.go http.go jar.go method.go request.go response.go server.go sniff.go status.go transfer.go transport.go] $ go list -f={{.TestGoFiles}} net/http [cookie_test.go export_test.go filetransport_test.go header_test.go http_test.go proxy_test.go range_test.go rearequest_test.go requestwrite_test.go response_test.go responsewrite_test.go transfer_test.go transport_internal_test.go] 

    想要了解更多的基础内容可以阅读 Golang testing docs

    1.4 脱离依赖

    定义单元测试概念的关键点就是,它能够脱离运行时的依赖项或合作者。

    这在 Go 语言中是通过接口来实现的,不过如果你有 C# 或者 Java 的背景,它们的接口看起来和 Go 会有些许不同。Go 语言中接口是隐含的,而不是一种强制措施。意味着实际的类并不需要知道接口的存在。

    这意味着我们可以定义非常多的小接口,如 io.ReadCloser 它只包含两个方法分别来自于 Reader 和 Closer 接口:

    Read(p []byte) (n int, err error) 

    Reader 接口

    Close() error 

    Closer 接口

    如果你在设计一个会被第三方使用的包,那么定义适当的接口就会显得非常有意义,因为其他人需要时,可以利用这些接口让单元测试代码能够不依赖于你的代码包。

    接口的具体实现在函数调用时可以被替换。如果我们想要测试这个方法,我们可以提供一个实现了 Reader 接口的伪造类。

    package main import ( "fmt" "io" ) type FakeReader struct { } func (FakeReader) Read(p []byte) (n int, err error) { // return an integer and error or nil } func ReadAllTheBytes(reader io.Reader) []byte { // read from the reader.. } func main() { fakeReader := FakeReader{} // You could create a method called SetFakeBytes which initialises canned data. fakeReader.SetFakeBytes([]byte("when called, return this data")) bytes := ReadAllTheBytes(fakeReader) fmt.Printf("%d bytes read.\n", len(bytes)) } 

    在实现你自己的抽象前,去 Golang 文档中搜索一下是否已有现成可用的东西,总会是个不错的主意。对于上面的例子我们也可以使用标准库中的 bytes 包:

    func NewReader(b []byte) *Reader 

    Go 语言的 testing/iotest 包提供了一些 Reader 的实现类,有些执行起来比较慢,有些会在读数据的中途产生错误。这些实现对于适应性测试都非常好用。

    查看原文

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2557 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 04:01 PVG 12:01 LAX 20:01 JFK 23:01
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86