1. 简介
下图出自 Learning Curves (for different programming languages) 虽然文章调侃为主, 但也看出来作者对单元测试的态度。
对Python程序员来讲,随着经验的增长, 在掌握了单元测试之后, 个人的生产力得到一个突变(至于掌握装饰器,会自我感觉膨胀,但效率提升不明显)。
很多年前刚走出校门,还是C++年代。 有个前北电的Java大牛布道,给开发团队推荐cpp-unit,说单元测试 “可以显著提高单兵作战能力”。斗转星移,南征北战,北电的Eddie已失去联系,但他的布道显然成功了。我在不同的项目中实践过单元测试:
- cpp unit
- junit
- luaunit
- python unittest
- jest
虽然语言不同,但unittest思想都一样,都有setup,teardown,testcase,assert/expect,mock这些概念。
对单元测试“提高单兵作战能力”的说法,我“不能认同更多”。
2. 单元测试的好处
unittest的好处主要在两个方面。
2.1. 鼓励先设计接口
要测试驱动,就要先想怎么测,从而促进在很早期就从接口定义的角度考虑问题。也促进了模块的低耦合高内聚。 另外,还带来一个额外的好处,TDD也会沉淀出类似文档的测试用例。若干年后回顾一个软件时候,看看用例,基本也了解当时的思路了。
2.2. 跑一遍测试的成本低
测试用例的写法规范,测试框架支持方便的执行和反馈结果,这样跑一遍测试没有时间和精力的负担,随时跑。 经常跑测试,持续的集成,有问题也能早暴露。 接口稳定后,有测试用例做质量保障,做重构也方便。
3. 举个例子
假如在开发一个API服务器。 做单元测试从粗到细可以有几个不同的粒度:
- 从API层面,用http client模拟请求,然后assert返回的结果是否符合预期
- 从Handler层面,模拟http请求的header,params,body等,然后喂给Handler,看返回的结构是否符合预期
- 模块层面,如果Handler之下还有其它业务逻辑模块,可以针对模块接口做单元测试
以上 1, 已经可以看作系统测试(端到端测试)了。
2相对于1有一些额外的好处:一般情况下,对应一个请求的处理,除了Hanlder之外,还有一些中间件来做预处理。 对于1来说,必须把中间件的功能和Handler本身作为一个整体测试。 不够灵活。
2相对于1又有一些缺点,需要Mock。 根据所用framework不同,需要mock输入输出的数据结构。 好在一般情况下,mock都比较简单,有很多framework也有第三方做好的mock库。 例如,下面对JS express 的Handler测试:
import { posthandler } from '../src/handlers/post';
import { getMockReq, getMockRes } from '@jest-mock/express';
// generate a mocked response and next function, with provided values
const { res, next } = getMockRes({
})
test('check post handler returns token in JSON body', async () => {
// generate a mock request with params
const req = getMockReq({ params: { id: 'abc-def' }, headers:{authorization:'this is my token'} })
// provide the mock req, res, and next to assert
await posthandler(req, res)
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
authorization: 'this is my token',
}),
)
})
引入的第三方jest-mock/express,可以帮助来生成输入数据,检查输出数据。see? easy.
4. 结论
大多数规模的项目,只要引入简单的几个概念,开发人员一两个小时就可以入门。 把对不同模块的assert组织到测试用例里,能方便的运行测试用例就很OK了。 投入小,产出高。初级程序员进阶必备。