1、单元测试是什么
单元测试可以检查我们的代码能否按照预期进行,代码逻辑是否有问题,以此可以提升代码质量。简单来说单元测试就是针对某一个函数方法进行测试,我们要先测试正确的传值与获取正确的预期结果,然后再添加更多测试用例,得出多种预期结果。尽可能达到该方法逻辑没有问题,或者问题都能被我们预知到。这就是单元测试的好处。
2、Golang 是怎么写单元测试的?
很简单!Golang 本身对自动化测试非常友好,并且有许多优秀的测试框架支持,非常好上手。文中将以新手的角度,快速上手一一实践给大家感受 Go 单元测试的魅力。
3、写一个最简单的 Test 吧!
先来了解一下 Go 官方的 testing 包
要编写一个测试文件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数,如上所述。 将该文件放在与被测试文件相同的包中。该文件将被排除在正常的程序包之外,但在运行 go test 命令时将被包含。
测试函数的签名必须接收一个指向 testing.T 类型的指针,并且不能返回任何值。函数名最好是 Test + 要测试的方法函数名。
一个简单的例子
这个代码将输出一句 “Hello, world”
测试
测试结果
说明我们的测试通过了,返回的值与我们预期的值是相同的。
现在尝试把预期结果修改一下
want := "xdcute.com"
测试结果
此时提示测试不通过,得到的值与预期的值不相同。
这就是一个最简单的测试写法,我们可以进行正确或错误的测试。
这里介绍几个常用的参数:
-bench:regexp 执行相应的 benchmarks,例如 -bench= (基准测试)
-cover:开启测试覆盖率
-run :regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数;
-v :显示测试的详细命令
GoConvey 测试框架
在前面,我们判断预期结果都是用 if…else 之类的判断语句,如果面对更庞大的测试,有更多的测试用例,可能需要思考更多逻辑判断,加大代码长度与复杂度,不便于编写与管理。所以我们需要用到更好的测试框架来增强测试编写。
GoConvey 是一款针对 Golang 的测试框架,它可以更好的管理和运行测试用例,而且又很丰富的断言函数,能够写出更完善的测试用例,并且还有 web 界面,是极其常用的测试框架。
安装
基本使用方法
下面是一个基本四则运算(加、减、乘、除)的代码:
下面为这 4 个函数分别书写单元测试:
TestTestAdd*testing.TConvey*testing.T
ConveyTestDivisionConveySoShouldBeNilShouldEqualShouldNotBeNil
运行测试
go test -v
我们可以看到,输出结果调理非常清晰,单元测试的代码写起来也非常优雅。那么,这就是全部吗?当然不是。GoConvey 还有非常舒适的 Web 界面提供给开发者来进行自动化的编译测试工作。
GoConvey断言
Web 界面
想要使用 GoConvey 的 Web 界面特性,需要在相应目录下执行 goconvey,然后打开浏览器,访问 http://localhost:8080 ,就可以看到下以下界面:
表格驱动测试
结合表格测试可以写多个测试用例。testing 本身也可以写表格测试,这里使用 Testify 演示。
测试代码
func TestCheckUrl3(t *testing.T) {
assert := assert.New(t)
var tests = []struct {
input string
expected bool
}{
{"xdcute.com", true},
{"xxx.com", false},
}
for _, test := range tests {
fmt.Println(test.input)
assert.Equal(CheckUrl(test.input), test.expected)
}
}
这就是关于 Testify 的快速上手,关于它的 mock 功能,将在后面引入 mock 概念后再介绍。
HttpMock - 模拟 http 请求
在 web 项目中,大多接口是处理 http 请求(post、get 之类的),可以利用官方自带的 http 包来进行模拟请求。
假如有一个 HttpGetWithTimeOut 方法,内部逻辑会有一个 get 请求,最后返回内容。我们在测试环境中,是访问不到它发起的 get 请求的 url 的,此时就可以模拟 http 请求来写测试。
代码示例:
func TestHttpGetWithTimeOut(t *testing.T) {
Convey("TestHttpGetWithTimeOut", t, func() {
Convey("TestHttpGetWithTimeOut normal", func() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("TestHttpGetWithTimeOut success!!"))
if r.Method != "GET" {
t.Errorf("Except 'Get' got '%s'", r.Method)
}
if r.URL.EscapedPath() != "/要访问的url" {
t.Errorf("Expected request to '/要访问的url', got '%s'", r.URL.EscapedPath())
}
}))
api := ts.URL
defer ts.Close()
var header = make(map[string]string)
HttpGetWithTimeOut(api, header, 30)
})
httptest.NewServer():创建一个 http 请求
http.ResponseWriter:响应体
http.Request:请求体
这段代码中,通过 w 来设置返回的头内容与写入内容,通过 r 来设置请求方法和请求的 url。
最后将模拟好的请求,传参对应方法。
SqlMock - 模拟数据库请求
特点:
1. 模拟任何实现了 sql/driver 接口的 db 驱动,无需关注 db 连接。
使用参考:
构建模拟 sql
db, mock, err = sqlmock.New() // mock sql.DB
defer db.Close()
执行查询语句
mock.ExpectQuery(sqlSelectAll).WillReturnRows(sqlmock.NewRows(nil))
有更多调用方法请查看官方文档
GoMonkey - 强大的打桩框架
特点:
1. 直接在方法级别上进行 mock
(在运行时通过汇编语句重写可执行文件,将待打桩函数或方法的实现跳转到桩实现)
在编译阶段直接替换掉真的函数代码部分
2. 非线程安全,请勿用于并发测试
使用:
PatchInstanceMethod() 对于方法
在使用前,先要定义一个目标类的指针变量 x
第一个参数是 reflect.TypeOf (x)
第二个参数是字符串形式的函数名
返回值是一个 PatchGuard 对象指针,主要用于在测试结束时删除当前的补丁
var e *Etcd
guard := PatchInstanceMethod(reflect.TypeOf(e), "Get", func(_ *Etcd, _ string) []string {
return []string{"task1", "task5", "task8"}
})
defer guard.Unpatch()
Patch() 对于过程
当一个函数没有返回值时,该函数我们一般称为过程。很多时候,我们将资源清理类函数定义为过程。
guard := Patch(DestroyResource, func(_ string) {
})
defer guard.Unpatch()
对于函数
第一个参数是目标函数的函数名
第二个参数是桩函数的函数名,习惯用法是匿名函数或闭包
返回值是一个 PatchGuard 对象指针,主要用于在测试结束时删除当前的补丁
func TestExec(t *testing.T) {
Convey("test has digit", t, func() {
Convey("for succ", func() {
outputExpect := "xxx-vethName100-yyy"
guard := Patch(osencap.Exec, func(_ string, _ ...string) (string, error) {
return outputExpect, nil
})
defer guard.Unpatch()
output, err := osencap.Exec(any, any)
So(output, ShouldEqual, outputExpect)
So(err, ShouldBeNil)
})
})
}