背景

​ 最近在工作和业余开源贡献中,和单元测试接触的比较频繁。但是在这两个场景之下写出来的单元测试貌似不太一样,即便是同一个代码场景,今天写出来的单元测试和昨天写的也不是很一样,我感受到了对于单元测试,我没有一个比较统一的规范和一套单元测试实践的方法论。在写了一些单元测试之后我开始想去了解写单元测试的一些最佳实践和技巧。(其实后来我反思的时候觉得,我应该先去学习单元测试相关的最佳实践,现有一个大致的概念,再去实操会好一些。)在这里总结成一篇文章分享给大家,希望读者朋友们有所收获。

1. 为什么要写单元测试

​ 单元测试是一个优秀项目必不可少的一部分,在一个频繁变动和多人合作的项目中显得尤为关键。站在写程序的人的角度出发,其实很多时候你并不能百分之百确定你的代码就是一点问题都没有的,在计算机的世界里其实不确定的因素很多,比如我们可能不确定代码中的一些依赖项,在实际代码执行的过程中他会符合我们的预期,我们也不能确定我们写的逻辑是否可以涵盖所有的场景,比如可能会存在写了if没有写else的情况。所以我们需要写自测去自证我们的代码没有问题,当然写自测也并不可以保证代码就完完全全没有问题了,只能说可以做到尽可能的避免问题吧。其次对于一个多人参与的项目来说,开源项目也好,工作中多人协作也好,如果要看懂一段逻辑是干嘛的,或者要了解代码是怎么运作的,最好的切入点往往是看这个项目的单元测试或者参与编写这个项目的单元测试。我个人要学习一个开源项目也是首先从单元测试入手的,单元测试可以告诉我一段逻辑这段代码是干什么的,他的预期是输入是什么,产出是什么,什么场景会报错。

2. 如何写好单元测试

​ 这一章节将会介绍为什么一些代码比较难以测试,以及如何写一个比较好的测试。在这里会结合一些我看过的一些开源项目的代码进行举例讲述。

2.1 什么代码比较难测试

​ 其实不是所有的代码都是可以测试的,或者说有的代码其实是不容易测试的,有时候为了方便测试,需要把代码重构成容易测试的样子。但是很多时候在写单元测试之前,你都不知道你写的代码其实是不可以测的。这里我举go-mysql的一些代码例子来阐述不可测或者不容易测的因素都有哪些。

​ go-mysql 是pingcap首席架构师唐刘大佬实现的一个mysql工具库,里面提供了一些实用的工具,比如canal模块可以消费mysql-binlog数据实现mysql数据的复制,client模块是一个简单的mysql驱动,实现与mysql的交互等等,其他功能可以去github上看readme详细介绍。最近由于工作需要看了大量这个库的源码,所以在这里拿一些代码出来举举例子。

2.1.1 代码依赖外部的环境

​ 在我们实际些代码的时候,实际一部分代码会比较依赖外部的环境,比如我们的一些逻辑可能会需要连接到mysql,或者你会需要一个tcp的连接。比如下面这段代码:

/*Conn is the base class to handle MySQL protocol.
*/
type Conn struct {net.ConnbufPool *BufPoolbr      *bufio.Readerreader  io.ReadercopyNBuf []byteheader [4]byteSequence uint8
}

​ 这个是go-msyql处理网络连接的结构体,我们可以看到的是这个结构体里面包裹的是一个net.Conn接口,并不是某一个具体的实现,这样子提供了很灵活的测试方式,只需要mock一个net.Conn的实现类就可以测试他的相关方法了,如果这里封装的是net.Conn的具体实现比如TCPConn,这样就变得不好测试了,在写单元测试的时候你可能需要给他提供一个TCP的环境,这样子其实比较麻烦了。

​ 第二个例子来自go-mysql canal这个模块,这个模块的主要功能通过消费mysql binlog的形式来复制mysql的数据,那么这里的整体逻辑怎么测试呢,这个模块是伪装成mysql的从节点去复制数据的,那么主节点在哪里呢,这里就要切切实实的mysql环境了。我们可以看看作者是怎么测试的,这里代码太长我就不贴出来了,把GitHub的代码链接贴在这里,感兴趣的读者可以去看点击这里看github代码。作者在CI环境里弄了一个mysql的环境,然后在测试之前通过执行一些sql语句来构建测试的环境,在测试的过程中也是通过执行sql的方式来产生对应的binlog去验证自己的逻辑。

2.1.2 代码太过冗余

​ 有时候写代码可能就是图个爽快,一把梭哈把所有的逻辑都放在一个函数里面,这样就会导致过多的逻辑堆积在一起,测试的时候分支可能过多,所以为了单元测试看起来比较简洁可能需要我们把这样的逻辑进行拆分,把专门做一件事情的逻辑放在一起,去做对应的测试。然后对整段逻辑做整体测试就好。

2.2 如何写好一个单元测试

​ 为了方便去描述这个一些内容,这里我简单的提供一个这样的函数。这个函数逻辑比较简单,就是输入一个名字,然后返回一个跟你打招呼的信息。

func Greeter(name string) string {return "hi " + name
}

​ 那么如何写这个函数的测试呢。我理解有两个关键的点,一是单元测试的命名,二是单元测试的内容架构。

2.2.1 单元测试的命名

​ 命名其实也是有讲究的,我理解单元测试也是给别人看的,所以当我看你写的单元测试的时候,最好在命名上有:测试对象,输入,预期输出。这样可以通过名字知道这个单元测试大致内容是什么。

2.2.2 测试内容架构

​ 测试的内容架构主要是这几件事情:

  1. 测试准备。在测试之前可能需要准备一些数据,mock一些入参。
  2. 执行。执行需要测试的代码。
  3. 验证。验证我们的逻辑对不对,这里主要做的是执行代码之后预期的返回和实际返回之间的一个比对。

所以综合上面两点,比较好的实践是这样的。

// 比较详细的写法,测试的是什么(Greeter), 入参是什么(elliot), 预期结果是什么(hi elliot)
func Test_Greeter_when_param_is_elliot_get_hi_Elliot(t *testing.T) {// 准备name := "elliot"// 执行greet := Greeter(name)// 验证assert.Equal(t, "hi elliot", greet)
}// 比较省略的写法,测试的是什么(Greeter), 入参是name, 预期结果是一个打招呼的msg,GreetMsg
func Test_Greeter_name_greetMsg(t *testing.T) {// 准备name := "elliot"// 执行greet := Greeter(name)// 验证assert.Equal(t, "hi elliot", greet)
}

这里要注意一个问题,尽量避免执行和验证的代码写在一起,比如写成这样子:

assert.Equal(t, "hi elliot", Greeter("elliot"))

这样子其实在功能上是一样的,但是会影响代码的可读性。不是特别推荐。

3. 什么是好的测试

​ 在讲了如何写一个单元测试之后,我们来说说什么的测试才是好的测试。我个人认为一个好的测试应该具备一下三点:

  1. 可信赖。首先我们写的单元测试的作用是测试某一段逻辑的正确性,如果我们的写的单元测试都是不值得信赖的,那么又如何保证测试的对象是值得信赖的呢?有时候一些单元测试也有可能时好时坏,比如一个单元测试依赖一个随机数去做一些逻辑,那么本身这个随机数就是不可控的,可能这下执行是好的,下一次执行就过不了了。
  2. 可维护。业务逻辑会不断的迭代,那么单元测试也会跟着不断的迭代,如果每次改单元测试都要花很多时间,那这个单元测试的可维护性就比较差了。其实把所有逻辑都塞在一个函数里,我个人认为这样子的代码对应的单元测试可维护性是比较差的,全部堆在一块意味着每次的改动所带来的单元测试的改动都需要兼顾全局的影响。如果尽可能的拆分开来,可以实现单元测试的按需改动。
  3. 可读性。最后的也是最重要的 就是单元测试的代码的可读性了,一个无法让人理解的单元测试其实和没写没什么区别,无法理解基本也以为着不可信赖和不可维护。我代码都看不懂怎么信任你呢?所以保障代码的可读性是很重要的。

​ 其实讲了一些概念之后对怎么样写好一个测试我们还是没什么印象的,那么可以从一些不好的case去入手,我们知道了那些实践是不好的之后,就会对好的实践有一个大致的认识。

  1. 可读性低的测试,上面写到的对greeter函数的单元测试中,其实这段代码写的不是很好的,可读性比较低。因为对于读这段代码的人来说,我都不知道这个“hi elliot”是什么,他为什么会出现在这里。如果把他稍微命名成一个变量的话可读性会高一些。
// 可读性比较低,因为读者并不知道这个“hi elliot”是什么
assert.Equal(t, "hi elliot", greet)// 这样就会好一些
expectedGreetMsg := "hi elliot"
assert.Equal(t, expectedGreetMsg, greet)
  1. 带有逻辑的测试。作为一个单元测试,应该尽量避免里面带有逻辑,如果有过多的逻辑在里面,那么就会演变成他本身也是需要测试的代码,因为过多的逻辑带来了更多的不可信赖。
  2. 有错误处理的测试。在单元测试中不要带有错误处理的逻辑,因为单元测试本身就是用来发现程序中的一些错误的,如果我们直接把panic给捕获了,那么也不知道代码是在哪里错的。另外对于单元测试来说错误应该也是一种预期的结果。
  3. 无法重现的测试。这个《单元测试的艺术》这本书里提供了一个比较有意思的例子。在代码中使用了随机数进行测试,每次产生的随机数都不一样,意味着每次测试的数据也就不一样了,这意味着这个测试代码可信赖程度比较低。
  4. 单元测试之间尽量隔离。尽量做到每个单元测试之间的数据都是自己准备的,尽量不要共用一套东西,因为这样做就意味着一个单元测试的成功与否与另外一个单元测试开始有了关联,不可控的东西就增加了。举个例子,nutsdb的单元测试有几个全局变量,其中大多数单元测试的db实例是共用的,如果上一个单元测试把db关闭了,或者修改了一些配置重启了db,对于下一个单元测试来说他是不知道别人操作了什么,等他执行的时候有可能就会出现意想不到的错误。
  5. 每一个单元测试都尽量独立。每个单元测试尽量可以独立运行。也不要有先后顺序,不要在一个测试里去调用另外一个测试。

在讲完大概比较好的单元测试实践之后,我们可以稍微提升一下。我们不妨假设有这么一个场景,其实是测一段逻辑,但是会有好几个测试用例需要测试,那么我们需要写好几个测试的函数嘛?其实是不用的,这里就涉及到了,参数化测试,什么意思呢?我们直接举例吧。看下面这段代码。

func isLargerThanTen(num int) bool {return num > 10
}func TestIsLargerThanTen_All(t *testing.T) {var tests = []struct {name     stringnum      intexpected bool}{{name:     "test_larger_than_ten",num:      11,expected: true,},{name:     "test_less_than_ten",num:      9,expected: false,},{name:     "test_equal_than_ten",num:      10,expected: false,},}for _, test := range tests {t.Run(test.name, func(t *testing.T) {res := isLargerThanTen(test.num)assert.Equal(t, test.expected, res)})}
}

​ 这里面测试的是一个判断入参是否大于10的函数,那么我们自然而然的想到三个测试用例,参数大于10的,等于10的,小于10的。但是实际上这三个测试用例都在测试一段逻辑,实际上是不太需要写三个函数的。所以把这三个测试用例和对应的预期结果封装起来,在for循环里面跑这三个测试用例。个人觉得这是一种比较好的测试方法。

3. go测试工具推荐

​ 在讲完上面的一些测试方法之后,在这里推荐一些在go里面的测试工具。其中最著名的testify就是不得不推荐的了。很多开源项目都在用这个库构建测试用例。说到这里突然想到之前有人给goleveldb提交pr代码写自己的单元测试时引入了这个库,我还“批斗”了他,说修改代码和引入新的库是两码事,请你分开做hhhh,现在想想还蛮不好意思的。回归正题,我们来简单介绍一些testify这个库。

3.1 testify

​ testify这个库主要有三个核心内容,assert, mock, suite。assert就是断言,可以封装了一些判断是否相等,是否会有异常之类的。文章篇幅有限,这里就不对assert的api一一介绍了,感兴趣的朋友们可以看衍生阅读的相关文章。这里我主要介绍mock和suite模块。

3.1.1 mock

在我们要准备测试的时候经常需要准备一些数据,mock模块通过实现接口的方式来伪造数据。从而在测试的时候可以用这个mock的对象作为参数进行传递。废话不多说我们看下怎么简单的实践一下。

首先我们定义一个接口:

//go:generate mockery --name=Man
type Man interface {GetName() stringIsHandSomeBoy() bool
}

这个接口定义了一个男孩子,一个方法是获取他的名字,第二个方法是看他是不是帅哥。这里我还推荐使用go:generate的方式执行mockery(执行go get -u -v github.com/vektra/mockery/…/安装)命令去生成对应的mock对象(生成的代码会放在当前目录的mocks目录下,当然你也可以在命令上添加参数指定生成路径),这样就不需要我们去实现mock对象的一些方法了。下面我们看下生成的代码是怎么样的。

// Code generated by mockery v2.10.0. DO NOT EDIT.package mocksimport mock "github.com/stretchr/testify/mock"// Man is an autogenerated mock type for the Man type
type Man struct {mock.Mock
}// GetName provides a mock function with given fields:
func (_m *Man) GetName() string {ret := _m.Called()var r0 stringif rf, ok := ret.Get(0).(func() string); ok {r0 = rf()} else {r0 = ret.Get(0).(string)}return r0
}// IsHandSomeBoy provides a mock function with given fields:
func (_m *Man) IsHandSomeBoy() bool {ret := _m.Called()var r0 boolif rf, ok := ret.Get(0).(func() bool); ok {r0 = rf()} else {r0 = ret.Get(0).(bool)}return r0
}

那么我们怎么使用呢?看看下面代码:

func TestMan_All(t *testing.T) {man := mocks.Man{}// 可以通过这段话来添加某个方法对应的返回man.On("GetName").Return("Elliot").On("IsHandSomeBoy").Return(true)assert.Equal(t, "Elliot", man.GetName())assert.Equal(t, true, man.IsHandSomeBoy())
}

3.1.2 suite

​ 有时候我们可能需要测的不是一个单独的函数,是一个对象的很多方法,比如想对leveldb的一些主要方法进行测试,比如简单的读写,范围查询,那么如果每个功能的单元测试都写成一个函数,那么可能这里会重复初始化一些东西,比如db。其实这里是可以做到共享一些状态的,比如数据写入之后就可以测试把这个数据读出来,或者范围查询。在这里的话其实用一种比较紧密的方式把他们串联起来会比较好。那么suite套件就应运而生。这里我就不打算在详细介绍了,感兴趣的读者可以移步衍生阅读中的《go每日一库之testify》。我理解这篇文章讲的比较清晰了。但是这里的话我可以提供nutsdb的一个相关测试用例大家参考:https://github.com/nutsdb/nutsdb/blob/master/bucket_meta_test.go 大家感兴趣的话也可以参考这段代码。

4. 总结

​ 这篇文章主要是总结最近我在单元测试上面的一些思考和沉淀,以及对go的测试工具的粗略讲解。在本文中使用到的一些开源项目的源码,主要是分享一些自己的思考,希望对大家有所帮助。

延伸阅读

  1. Best Practices for Testing in Go:https://fossa.com/blog/golang-best-practices-testing-go/#Catch
  2. 《单元测试的艺术》
  3. go每日一库之testify:https://segmentfault.com/a/1190000040501767
  4. 使用testify和mockery库简化单元测试:https://segmentfault.com/a/1190000016897506

如何写好测试用例以及go单元测试工具testify简单介绍相关推荐

  1. 多通道振弦传感器无线采集仪参数配置工具SETP简单介绍

    多通道振弦传感器无线采集仪参数配置工具SETP简单介绍 VS208/416/432 是以振弦.温度传感信号为主的多通道无线采集仪,并可扩展其它 模拟(电流.电压.电阻)信号和数字信号(RS485.RS ...

  2. java 单元测试用例_Java之单元测试工具(Junit)

    Junit是Java单元测试框架,一般Eclipse里面会集成这个Junit4测试工具 既然是测试工具,虽然开发用得比较多,但作为测试人员也需要具备会Junit测试的思想,况且技多不压身 这里简单介绍 ...

  3. Postman工具的简单介绍

    Postman工具简单介绍 前言 一.侧边栏 二.工具栏 三.构建器 四.控制台 五.菜单栏 六.状态栏 七.选项卡和窗口 八.键盘快捷键 九.数据编辑器 十.支持多行 前言 Postman提供了一种 ...

  4. IOS手机自动化一些工具的简单介绍

    工具1XCTest     XCTest是苹果在iOS 7和Xcode5引入的一个简单而强大的测试框架,它的测试编写起来非常简单,并且遵循xUnit风格.XCTest的优点是与Xcode深度集成,有专 ...

  5. 安卓手机自动化一些工具的简单介绍

    工具1Monkey: 工具介绍:Monkey是一个程序,运行在您的模拟器或设备上,并生成伪随机的用户事件流,如点击,触摸或手势,以及一些系统级事件. 您可以使用Monkey以随机但可重复的方式对您正在 ...

  6. java 测试工具 oracle_几种测试工具的简单介绍

    负载测试(Load Test):负载测试是一种性能测试,指数据在超负荷环境中运行,程序是否能够承担. 二.WinRunner WinRunner 是一种企业级的功能测试工具,用于检验企业应用程序是否能 ...

  7. 大数据离线分析工具Hive简单介绍

    Hive是Facebook为了解决海量日志数据的分析而开发的,后来开源给了Apache软件基金会,可见Apache软件基金会是个神奇的组织,我们之前学过的很多开源工具都有Apache软件基金会的身影. ...

  8. otrs软件_开源ITIL管理工具OTRS简单介绍

    OTRS的名字是由Open-source Ticket Request System首字母縮略字而来,是一个开源的缺陷跟踪管理系统软件.OTRS将电话,邮件等各种渠道提交进来的服务请求归类为不同的队列 ...

  9. 快速跳转工具--FASD 简单介绍

    前言 fasd是一个命令行加速工具.它提供了对文件和文件夹的快速访问.它和autojmp, z, v都很相近.它会记录你访问过的文件夹和文件, 然后你就可以通过简短的名字来直接访问它们. fasd会对 ...

最新文章

  1. 记一次信息泄露(被美团泄露出去的)和被诈骗经历
  2. MNIST 训练测试
  3. C/C++程序从编译到最终生成可执行文件的过程分析
  4. QT 生成二维码接口封装
  5. python web框架 多线程_python 简单web框架: Bottle
  6. (转)Asp.net 中 Get和Post 的用法
  7. Golang QRCode 生成实现
  8. 帝国CMS系统目录结构介绍
  9. WIN2003 装不上mssql2000
  10. 高三计算机教学计划,精选高三教学计划三篇
  11. Portal是什么东东
  12. 从0开始学大数据(十二)
  13. PHP实现opentracing链路追踪
  14. excel表格怎么拆分成多个表格?
  15. Python实现群发邮件
  16. Kubernetes 管理员认证(CKA)考试笔记(四)
  17. python time localtime()
  18. 预定义类型未定义或导入_【小程序】商家来看看,常见的微信小程序类型有哪些?...
  19. 发现一款好用到爆的数据库工具,被惊艳到了!
  20. 2022全球智博会 打开非凡之城的未来“穿越门”

热门文章

  1. 【机器学习sklearn】主成分分析PCA(Principal Component Analysis)
  2. 矩阵的基变换及对应基变换下向量的坐标变换
  3. 实现某位置附近距离【Redis的GEO】
  4. JAVA-SUST实验二 JavaBean组件程序设计
  5. 通过使用5个开源的人脸识别项目来增加你的计算机视觉项目经历
  6. 逃脱“黑天鹅”魔咒:荣耀逆势增长背后的“反脆弱”之力
  7. linux系统键盘被锁定,在Linux下锁住键盘和鼠标而不锁屏
  8. Web网页设计之jQuery_1. 认识jQuery
  9. 从无栈协程到C++异步框架
  10. 谷歌浏览器主页图片设置