Go 单元测试实践
date
Dec 10, 2023
slug
ut
status
Published
tags
单元测试
summary
单元测试使用经验。
type
Post
工具
mock 工具,帮助你生成 mock 对象,例如网络请求,DB 请求。
convey 帮助你组织单元测试。
goconvey
smartystreets • Updated Dec 23, 2023
Go 本身就支持单元测试,但我一般习惯使用 convey 来编写单测,文章中的单测都使用 convey 组织。
单元测试是什么
单元测试是一种软件测试方法,它验证代码中的最小可测试单元(如函数或方法)是否按预期工作。在后端开发中,单元测试用于检查给定输入时是否能返回期望的输出。
例如,有一个函数
Add(a int, b int) int
用于求和,一个相关的单元测试可能会检查Add(2, 3)
是否返回5
。为什么要写单测
错误识别:可以在早期阶段及时发现代码中的问题或错误。
一个常见的错误,如果 u 是 nil,那么将会 panic:
而且这种错误往往隐藏在大量的代码变更之中,在进行 code review 的时候,不容易被发现。
但是假如我们有写单测的习惯,这个问题是很容易被发现的:
通过第二个测试,可以很快地现 GetUserName 有 bug。
简化调试:当测试失败时,只需要查看较小的代码片段,使得找到问题更加容易。
当我们测试对象变得很复杂的时候,手动 debug 需要很长的流程才能找到问题所在,而运行测试用例,可以帮助你快速定位到问题所在:
设计改进:编写测试有助于理清楚函数或方法的预期行为,有助于改善系统设计。
当我们编写单元测试时,需要考虑测试对象在接收特定输入时应该如何表现。这个过程迫使我们从调用者的角度思考它们的设计,而不仅仅是实现。
例如,我们正在编写一个函数
CalculateDiscount(price float64, discountRate float64) float64
,该函数根据折扣率计算商品的最终价格。开始编写测试用例时,你可能会考虑如下几种情况:
- 如果
price
或discountRate
为负数,函数应该如何反应?
- 折扣率应在0到1之间,超出这个范围应该怎么处理?
- 如果
price
为0,无论折扣率是多少,返回值都应该是0。
对此,我们先写出测试用例:
然后,按照满足测试用例的目标,再去写实现,对实现过程非常有帮助。因为我们已经知道了预期的结果,可以照着这个结果努力。
这其实就很像算法题,假如你做过 leetcode 你就会知道,一道算法题如果只有输入,没有给定什么是合格的输出结果,或者输出结果互相矛盾,那么这道题根本没法做。只有同时给定和合法的输入、输出之后,算法题才有求解的可能性。把这个思想放到接口实现中也是一样的,我们没有单测的指引,其实也是照着自己的思维惯性去写接口实现,接口很容易输出预料之外的结果。
安全的重构:拥有充足的单元测试,可以让重构或优化代码变得更加安全,防止修改过程中引入新的bug。
举个例子,假如我们之前要重构一个接口,而这个接口完全没有测试用例,那么当你重构完毕,你如何保证这个接口没有破坏之前的逻辑呢?
假如这个接口非常重要,调用者非常多,对每个场景进行测试将是一项沉重的工作,且上线这个接口也会非常危险。
但是如果有了单测,重构的难度将会大大降低。如果针对该接口编写好了 10 个测试用例,那么当重构完毕后,只需要再运行一次这些测试用例就行了。
如果通过了所有的测试用例,那么这次就是一次成功的重构。这相当于对算法题换了一种求解方法,且这个求解方法能满足所有测试数据。
减少测试成本:长期来看,单元测试可以帮助找出问题,减少手动测试和调试的时间,降低测试成本。
写单测和开发的时间大约是 1:1,如果不写单测,虽然看起来是减少了一半的工作量,但是在长远来看,维护不写单测的接口成本是非常高的,因为每次变更都需要对它进行手动测试,重复的手动测试成本将会高于单元测试。
还有很多软件测试的理论强调了这一点,大家感兴趣可以自己搜索下。
持续集成/持续部署(CI/CD)友好:单元测试适合自动化,并且与CI/CD流程相结合能够提高软件交付的速度和质量。
没有测试,就没办法保证交付的速度和质量。尽管编写单元测试需要额外的时间和资源,尤其是在项目的初期阶段。然而,从长远来看,这种投资可以带来许多收益,包括提高开发速度和代码质量。
单测还有很多优点,这里就不一一展开了。
单测需要准备什么
- 理解需求:首先,我们需要清楚地理解要测试的函数或方法应该完成什么功能,明确其预期的行为。
- 测试框架:在Go语言中,标准库已经内置了测试框架,因此不需要额外的安装。我们只需要创建一个以
_test.go
结尾的文件即可。
- Mock工具:当需要测试与其他系统交互的代码时,比如数据库或者第三方服务等,你可能需要用到Mock工具来模拟这些系统的行为。
- 测试数据和测试用例:我们需要准备各种测试用例,包括正常的路径(希望发生的情况)和边缘情况、错误处理等。这通常需要一些输入数据和预期的输出结果。
后端服务如何写单测
一个典型的后端服务调用链:
middleware ⇒ server ⇒ service(biz层,业务逻辑) ⇒ data
middleware 主要做鉴权、请求限制等操作,一般不做单测。
server 层主要做参数校验,重组参数等逻辑,然后传递给biz层。
可以对其中要用到的工具进行单测,例如之前提到的
GetUserName
。data 层主要是使用 ORM 工具和 DB 交互,例如 ent、GORM等。一般来说 ORM 工具都经过充分测试,不需要写单测。
biz 层是需要单测的重点对象,往往是业务逻辑在此实现。
流程
- 先写接口和接口的函数,定义行为。
- 生成 Mock 接口,我喜欢用 go mockery 来实现。
- 写单元测试,使用 convey 搭配 mock,完成测试用例。
- 在有测试用例的情况下,写函数实现。
误区
- 先写实现,再写单测 ❌
补单测这回事不存在的。
- 满足单测一定就是正确的实现。
不一定,单元测试是我们自己写的,有可能覆盖得不够全面。
总结
在软件开发过程中,编写单元测试具有很多的优点,它不仅能确保代码的稳健性和可靠性,还能提升我们系统的健壮性,为后续的功能扩展铺平道路。因此,在足够的时间充裕下,一开始就编写单元测试将会极大地提高我们的工作效率并增强代码质量。