单元测试的概念
测试是专业主义的保障。请注意,测试不是程序员的额外工作,程序员写单元测试是为了确定系统真的可用,只有交付了可用的系统,程序员才算完成,业务方才能满足。所以,不要把它看作额外的工作,而应当看成节省时间和金钱的方法。单元测试的好处,一是可以减少返工时间,二是让开发人员可以更有底气的交付系统。
单元测试的基本要求:
- 可以快速运行测试
- 可以在不同环境下测试
- 可以并行测试
单元测试还是TDD的基础,TDD的概念不在此处赘述,如果感兴趣可选读这篇文章:juejin.cn/post/693617…
Go提供了强大的单元测试库testing,需要时也可以结合Gomock使用。
Package testing
go test
在Go中所有以 _test.go
为后缀名的源代码文件都属于测试文件。这部分文件在运行时会被Go忽略,并且可以使用go test
命令执行测试文件中的所有测试用例。
go test
携带-v
参数表示输出完整的测试结果。
go test
携带 -cover
参数可以查看测试覆盖率。
go test
携带 -coverprofile=c.out
参数可以将相关信息输出到当前文件夹下面的c.out
文件中。
如果只需要测试某几个函数,可以携带 -run
参数,它对应一个正则表达式,例如运行go test -run=Set
命令就只会执行函数名中带Set
的测试函数。
测试函数必须以Test开头,例如:
func TestName(t *testing.T) {
// todo
}
复制代码
一个简单的单元测试示例
产品代码示例:
package utils
func StrListToString(strList []string) (str string) {
if len(strList) > 0 {
for k, v := range strList {
if k == 0 {
str = v
} else {
str = str + "," + v
}
}
return
}
return ""
}
复制代码
测试代码示例:
package utils
import (
"reflect"
"testing"
)
type testStrListToStringCase struct {
strList []string
want string
}
var testStrListToStringGroup = []testStrListToStringCase{
// 测试用例1:单个英文
{
strList: []string{"str1"},
want: "str1",
},
// 测试用例2:多个英文
{
strList: []string{"str1", "str2", "str3"},
want: "str1,str2,str3",
},
// 测试用例3:逗号
{
strList: []string{"a,b,c"},
want: "a,b,c",
},
// 测试用例4:汉字
{
strList: []string{"一二三四五"},
want: "一二三四五",
},
// 测试用例5:空字符串
{
strList: []string{""},
want: "",
},
}
func TestStrListToString(t *testing.T) {
for _, test := range testStrListToStringGroup {
got := StrListToString(test.strList)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("The values of %v is not %v\n", got, test.want)
}
}
}
复制代码
我们定义了测试函数TestStrListToString和一个结构体数组testStrListToStringGroup,结构体数组testStrListToStringGroup包含了我们需要的所有测试用例,结构体中的strList字段作为输入的参数,want字段作为我们想要的结果。
在测试函数中循环输入测试用例,执行我们的产品代码,得到的结果可以用反射包的DeepEqual方法来判等,如果测试函数返回的结果和我们想要的不一致,就打印错误提示。
使用IDE自动生成测试代码
一些IDE支持自动生成测试代码的框架,比如可以使用goland编辑器的快捷键command+shift+t 自动生成测试代码(函数、文件、包)。
子测试
在测试函数中可以运行多组测试,使用t.Run(测试用例, 测试函数)
方法即可。
Go官方标准库中的fmt包有如下测试:
var flagtests = []struct {
in string
out string
}{
{"%a", "[%a]"},
{"%-a", "[%-a]"},
{"%+a", "[%+a]"},
{"%#a", "[%#a]"},
{"% a", "[% a]"},
{"%0a", "[%0a]"},
{"%1.2a", "[%1.2a]"},
{"%-1.2a", "[%-1.2a]"},
{"%+1.2a", "[%+1.2a]"},
{"%-+1.2a", "[%+-1.2a]"},
{"%-+1.2abc", "[%+-1.2a]bc"},
{"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
var flagprinter flagPrinter
for _, tt := range flagtests {
t.Run(tt.in, func(t *testing.T) {
s := Sprintf(tt.in, &flagprinter)
if s != tt.out {
t.Errorf("got %q, want %q", s, tt.out)
}
})
}
}
复制代码
上例定义了测试用例组flagtests,在测试函数TestFlagParser中使用了子测试方法t.Run。
Package httptest
网络测试
除了testing包,Go还提供了专门用于测试HTTP请求的httptest包。
请看以下示例:
func TestHandler(t *testing.T) {
// todo
r := SetupRouter()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(
"POST",
"/test",
)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]string
err := json.Unmarshal([]byte(w.Body.String()), &resp)
assert.Nil(t, err)
assert.Equal(t, tt.expect, resp["msg"])
})
}
}
复制代码
我们先用SetupRouter方法创建了一个路由对象,使用不同的web框架会用到不同的方法,只要保证此处能拿到Router对象就行。
在子测试t.Run中,我们用httptest.NewRequest方法mock了一个http请求,再用httptest.NewRecorder方法mock了一个响应记录器。
接着执行路由对象下的ServeHTTP方法调用接口,解析响应体w.Body即得到接口返回的内容。
在代码外调用api测试
上例使用了httptest包在代码内部调用请求进行测试,如果想要在外部直接调用api进行测试,可以使用接口文档工具Yapi或API管理工具Postman等。也可以用Go代码写一下GET和POST的网络请求直接调用api。