在上一篇文章 "Grab JSON from an API" 中,我们探索了如何使用 HTTP 客户端以及如何解析 JSON 数据。本篇文章是 Go 语言主题的续篇,讲述如何编写单元测试。
Go 语言有一个自带的测试命令 go test
,还有一个标准 testing
测试包,它能够为你提供一个小却完整的测试体验。
这套标准工具链还包括了基准测试以及基于语句的代码覆盖率测试,类似与 NCover(.Net) 或者 Istanbul(Node.js)。
和 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 或者 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
有两种方式可以用来启动一个包内的测试代码。这些方法对于单元测试和集成测试是相同的。
在和测试文件相同的目录中:
go test
这会执行包内所有匹配 _test.go 名称的测试代码
或者
采用完整的包名
go test github.com/alexellis/golangbasics1
现在你可以执行 Go 语言单元测试了,可以使用 go test -v
获得更详细的输出,你能看到每条测试的 PASS/FAIL 信息,以及所有 t.Log
打印出的额外日志信息。
单元测试和集成测试的区别在于,单元测试通常独立于外部依赖,不会与网络、磁盘等产生交互。单元测试一般只关注函数的功能。
go test
的更多用法go test
工具自带内建的代码语句覆盖率测试功能。想要用之前的代码例子尝试一下,输入以下命令即可:
$ go test -cover
PASS
coverage: 50.0% of statements
ok github.com/alexellis/golangbasics1 0.009s
较高的语句覆盖率比低覆盖率或者零覆盖率要好,不过这样量化也可能会产生误导。我们想保证我们不只是在执行语句,而且我们还验证了代码的行为和输出,而且在不符合逻辑的地方报错。如果你删除了之前例子代码中的 “ if ” 语句,它仍然会保持 50% 的测试覆盖率,却丧失了验证 “ Sum ” 方法行为的用处。
如果你使用接下来的两条命令,你就可以直观地看到你的程序哪些部分被覆盖到了,而哪些语句没有被覆盖到:
go test -cover -coverprofile=c.out
go tool cover -html=c.out -o coverage.html
然后用浏览器打开 coverage.html 文件。
还有一点,将 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 readrequest_test.go requestwrite_test.go response_test.go responsewrite_test.go transfer_test.go transport_internal_test.go]
想要了解更多的基础内容可以阅读 Golang testing docs。
定义单元测试概念的关键点就是,它能够脱离运行时的依赖项或合作者。
这在 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 的实现类,有些执行起来比较慢,有些会在读数据的中途产生错误。这些实现对于适应性测试都非常好用。