腾讯云技术社区-掘金主页持续为你们呈现云计算技术文章,欢迎你们关注!javascript
做者介绍:熊训德(英文名:Sundy),16年毕业于四川大学大学并加入腾讯。目前在腾讯云从事hadoop生态相关的云存储和计算等后台开发,喜欢并专一于研究大数据、虚拟化和人工智能等相关技术。html
本文档说明go语言自带的测试框架未提供或者未方便地提供的测试方案,主要是用于解决写单元测试中比较头痛的依赖问题。也就是伪造模式,经典的伪造模式有桩对象(stub),模拟对象(mock)和伪对象(fake)。比较幸运的是,社区有丰富的第三方测试框架支持支持。下面就对笔者亲身试用并实践到项目中的几个框架作介绍:java
gomock模拟对象的方式是让用户声明一个接口,而后使用gomock提供的mockgen工具生成mock对象代码。要模拟(mock)被测试代码的依赖对象时候,便可使用mock出来的对象来模拟和记录依赖对象的各类行为:好比最经常使用的返回值,调用次数等等。文字叙述有点抽象,直接上代码:git
dick.go中DickFunc依赖外部对象OutterObj,本示例就是说明如何使用gomock框架控制所依赖的对象。github
func DickFunc( outterObj MockInterface,para int)(result int){
fmt.Println("This init DickFunc")
fmt.Println("call outter.func:")
return outterObj.OutterFunc(para)
}复制代码
mockgen工具命令是:golang
mockgen -source {source_file}.go -destination {dest_file}.go
sql
好比,本示例便是:数据库
mockgen -source src_mock.go -destination dst_mock.go
编程
执行完后,可在同目录下找到生成的dst_mock.go文件,能够看到mockgen工具也实现了接口:
接下来就可使用mockgen工具生成的NewMockInterFace来生产mock对象,使用这个mock对象。OutterFunc()这个函数,gomock在控制mock类时支持链式编程的方式,其原理和其余链式编程相似一直维持了一个Call对象,把须要控制的方法名,入参,出参,调用次数以及前置和后置动做等,最后使用反射来调用方法,因此这个Call对象是mock对象的代理。jmockit的早期版本也是jdk自带的java.reflect.Proxy动态代理实现的(最近的版本是动态Instrumentation配合代理模式)。
在本示例中只简单的更改了返回值,抛砖引玉:
func TestDickFunc(t *testing.T ){
mockCtrl := gomock.NewController(t)
//defer mockCtrl.Finish()
mockObj := dick.NewMockMockInterface(mockCtrl)
mockObj.EXPECT().OutterFunc(3).Return(10)
result :=dick.DickFunc(mockObj,3)
t.Log("resutl:",result)
}复制代码
使用go test命令执行这个单测
从结果看:原本应该输出3,最后输出就是10,和其余语言mock框架类似,生产出来的Mock对象不用本身去重定义这么麻烦。
更多示例能够查看官网一个囊括gomock几乎全部功能的例子:
因为go在网络架构上的优秀封装,使得go在不少网络场景被普遍使用,而http协议是其中重要部分,在面对http请求的时候,能够对http的client进行测试,算是mock的特殊应用场景。
看一个简单的示例就轻松的看懂了:
func TestHttp(t *testing.T) {
handler := FruitServer()
server := httptest.NewServer(handler)
defer server.Close()
e := httpexpect.New(t, server.URL)
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Empty()
}复制代码
其中还支持对不一样方法(包括Header,Post等)的构造以及返回值Json的自定义,更多细节查看其官网
还有一个testify使用起来能够说兼容了《一》中的gocheck和gomock,可是其mock使用稍微有点烦杂,使用继承tetify.Mock(匿名组合)从新实现须要Mock的接口,在这个接口里使用者本身使用Called(反射实现)被Mock的接口。
《单元测试的艺术》中认为stub和mock最大的区别就依赖对象是否和被测对象有交互,而从结果看就是桩对象不会使测试失败,它只是为被测对象提供依赖的对象,并不改变测试结果,而mock则会根据不一样的交互测试要求,极可能会更改测试的结果。说了这么多理论,但其实这两种方法都不是割裂的,因此gomock框架除了像其名字同样能够模拟对象之外,还提供了桩对象的功能(stub)。以其实现来讲,更像是一个桩对象的注入。可是由于兼容了多个有用的功能,因此其在社区最为火爆。
具体用法可参考其github主页
还有一种比较常见的场景就是和数据库的交互场景,go-sqlmock是sql模拟(Mock)驱动器,主要用于测试数据库的交互,go-sqlmock提供了完整的事务的执行测试框架,最新的版本(16.11.02)还支持prepare参数化提交和执行的Mock方案。
好比有这样的被测函数:
func recordStats(db *sql.DB, userID, productID int64) (err error) {
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
switch err {
case nil:
err = tx.Commit()
default:
tx.Rollback()
}
}()
if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
return
}
if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
return
}
return
}
func main() {
db, err := sql.Open("mysql", "root@/root")
if err != nil {
panic(err)
}
defer db.Close()
if err = recordStats(db, 1 , 5 ); err != nil {
panic(err)
}
}复制代码
单测时:
func TestShouldUpdateStats(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("mock error: '%s' ", err)
}
defer db.Close()
mock.ExpectBegin()
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO product_viewers")
.WithArgs(2, 3)
.WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
if err = recordStats(db, 2, 3); err != nil {
t.Errorf("exe error: %s", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("not implements: %s", err)
}
}
//测试回滚
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("mock error: '%s'", err)
}
defer db.Close()
mock.ExpectBegin()
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO product_viewers")
.WithArgs(2, 3)
.WillReturnError(fmt.Errorf("some error"))
mock.ExpectRollback()
// 执行被测方法,有错
if err = recordStats(db, 2, 3); err == nil {
t.Errorf("not error")
}
// 执行被测方法,mock对象
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("not implements: %s", err)
}
}复制代码
更多例子和详情,请查看官网:
介绍了这么多框架,最后须要说明的也可能最重要的是写代码时就应该考虑代码是可被测试的。要使得单元测试容易写,或者说代码容易被测,其实很重要的一个部分就是被测代码自己是容易被测的,也就是说在设计和编写代码的时候就应该先想到相好如何单元测试,甚至有人提出能够先写单元测试,再写具体被测代码。由于一个接口(或者称为单元)在被设计好后,它实现就肯定了,实际效果也肯定了。这种方式被称做测试驱动开发(Test-Driven Development, TDD)。而对于已经写好的代码,很大程度上很差测试,有一种方式是测试性重构,就是为了更好的测试而进行重构。这些必定程度上来讲并了解这些框架更重要,有意向能够,能够查阅有关两本书《单元测试的艺术(第2版)》《xUnit测试模式》
参考:
codethoughts.info/go/2015/04/…《单元测试的艺术》
《xUnit测试模式》
相关阅读:
go单元测试基本篇
【腾讯TMQ】敏捷测试-快速俘虏产品&开发