Golang basics - writing unit tests
1. Go测试
Go有一个内建的测试指令go test
以及testing
包,联合给出一个最小但完整的测试体验。
标准工具链同时包含性能测试和基于语句的测试,代码覆盖率类似于NCover(.NET
)或Istanbul(Node.js
).
1.1 编写单元测试
Go中单元测试与语言中的其他特性例如格式化或命名一样,遵循固有的语法。语法故意避免使用断言,开发人员需对值和行为进行检查。
下面有个在main
包中的待测试的示例方法。我们定义了一个名为Sum
的导出函数,它接收两个整数,并将它们相加。
package main
func Sum(x int, y int) int {
return x + y
}
func main() {
Sum(5, 5)
}
然后我们在一个独立的文件中编写测试用例。测试文件可以在一个不同的package(文件夹)或在同一个(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)
}
}
Golang测试函数的特性:
- 第一个也是唯一的参数必须是
t *testing.T
- 函数名称以
Test
开头,紧接着以大写字母开头的单词或短语 - 通常被测试的方法会长这样 i.e.
TestValidateClient
- 调用
t.Error
或者t.Fail
来表示错误(例子中调用t.Errorf
来提供更多细节) -
t.Log
可以用来提供无失败的调试信息 - 测试代码必须保持在一个命名为
something_test.go
的文件中,例如:addition_test.go
如果你的代码和测试用例在用一个文件夹,那么你可以使用
go run *.go
语句来执行测试用例。我倾向于使用go build
创建一个二进制文件,然后再运行。
也许你更习惯于使用Assert
关键字来执行检查,但是The Go Programming Language
的作者对于超越断言的风格提出了一些好的论点。
使用断言:
- 用例会感觉是用不同的语言在编写(RSpec/Mocha用于实例化)
- 错误被隐藏在 "assert: 0 == 1"
- 生成好几页的堆栈跟踪信息
- 测试执行会在出现第一个断言后退出-掩盖了大量的错误
有其他第三方库赋值了RSpec或Assert的感受。可以查看stretchr/testify
测试表
"test tables"的概念是一系列测试输入和输出数值(切片数组)。这有一个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
启动测试
有两种方法启动一个package的测试。单元测试和整体测试的方法很类似。
1.同目录的测试:
go test
这个命令适配所有符合packagename_test.go
的文件
2.完全限定包名
go test github.com/alexellis/golangbasics1
现在在Go里运行了单元测试,go test -v
获取更加冗长的输出,并且你将看到每个测试用例的PASS/FAIL结果包括任何由t.Log
产生的日志信息。
单元测试和整体测试的区别是单元测试通常隔离与网络、磁盘等通信的依赖关系。单元测试通常仅仅测试一件事情,例如一个函数的功能。
1.2 更多有关go test
语句覆盖
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
的文件遗留在你的package也许会让人觉得不自然。REST确保GO编译器和链接器不会将您的测试文件存储在它所产生的任何二进制文件中 。
下面是在前面的Golang基础教程中使用的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
1.3 隔离依赖关系
定义单元测试的关键因素是与运行时依赖项或协作者隔离。
Golang中通过interface实现,如果你具有C#或Java背景,它们看起来有点不同。Golang中接口是隐含的而不是强制接口,这意味着具体类不需要提前知道接口。
这意味着我们可以拥有非常小的接口,例如 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
Golang testing/ iotest包提供了一些读取器实现,这些实现很慢或导致错误在读取中途被抛出。这些是弹性测试的理想选择
1.4 有用的示例
我将重构前一篇文章中的代码示例,其中我们发现有多少宇航员在太空中。
我们将从测试文件开始:
package main
import "testing"
type testWebRequest struct {
}
func (testWebRequest) FetchBytes(url string) []byte {
return []byte(`{"number": 2}`)
}
func TestGetAstronauts(t *testing.T) {
amount := GetAstronauts(testWebRequest{})
if amount != 1 {
t.Errorf("People in space, got: %d, want: %d.", amount, 1)
}
}
我有一个名为GetAstronauts的导出方法,它调用HTTP端点,从结果中读取字节,然后将其解析为结构并返回“number”属性中的整数
我在测试中的假/测试双重只返回满足测试所需的最小JSON,并且首先我让它返回一个不同的数字,以便我知道测试有在工作。很难确定第一次通过的测试是否有效。
这是我们运行主要功能的应用程序代码。 GetAstronauts函数将接口作为其第一个参数,允许我们从该文件及其导入列表中隔离和抽象出任何HTTP逻辑。
package main
import (
"encoding/json"
"fmt"
"log"
)
func GetAstronauts(getWebRequest GetWebRequest) int {
url := "http://api.open-notify.org/astros.json"
bodyBytes := getWebRequest.FetchBytes(url)
peopleResult := people{}
jsonErr := json.Unmarshal(bodyBytes, &peopleResult)
if jsonErr != nil {
log.Fatal(jsonErr)
}
return peopleResult.Number
}
func main() {
liveClient := LiveGetWebRequest{}
number := GetAstronauts(liveClient)
fmt.Println(number)
}
GetWebRequest接口指定如下函数:
type GetWebRequest interface {
FetchBytes(url string) []byte
}
接口是推断而不是显式修饰到结构上。这与C#或Java等语言不同。
名为types.go的完整文件看起来像这样,并从之前的博客文章中提取:
package main
import (
"io/ioutil"
"log"
"net/http"
"time"
)
type people struct {
Number int `json:"number"`
}
type GetWebRequest interface {
FetchBytes(url string) []byte
}
type LiveGetWebRequest struct {
}
func (LiveGetWebRequest) FetchBytes(url string) []byte {
spaceClient := http.Client{
Timeout: time.Second * 2, // Maximum of 2 secs
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("User-Agent", "spacecount-tutorial")
res, getErr := spaceClient.Do(req)
if getErr != nil {
log.Fatal(getErr)
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
log.Fatal(readErr)
}
return body
}
选择抽象的内容
上面的单元测试实际上只测试了json.Unmarshal函数以及我们对有效HTTP响应体的外观的假设。对于我们的示例,这种抽象可能没问题,但我们的代码覆盖率得分会很低。
也可以进行较低级别的测试,以确保正确执行HTTP获取超时2秒,或者我们创建了GET请求而不是POST
幸运的是,Go有一组用于创建虚假HTTP服务器和客户端的辅助函数
更进一步:
- 探索 http/httptest package
- 并重构上面的测试以使用虚假的HTTP客户端
- 之前和之后的测试覆盖百分比是多少?