本文是作者在项目组内推行单元测试的一些心得总结。
单元测试(Unit Test)是什么
针对方法/函数进行独立测试,验证结果是否符合编写预期。
TDD:测试驱动
BDD:行为驱动
原则
- Fast — tests should be able to be executed often. 快速 – 应该随时和经常执行单测
- Isolated — tests on their own cannot depend on external factors or on the result of another test. 独立 – 单测应该无需依赖外部实例或者依赖其他的测试结果
- Repeatable — tests should have the same result every time we run them. 可重复 – 在不改动的情况下,多次执行应该返回相同的结果
- Self-verifying — tests should include assertions; no human intervention needed. 自证 – 测试自身包括断言,无需人为干预
- Timely — tests should be written along with the production code. 时效 – 单测应该随着开发进行伴随着成长
单元测试 测什么/不测什么
测什么
- 公开方法
- 依赖外部输入(网络请求、数据存储、交互操作)
- 工具方法
公开方法(public api)是提供给外部(其他实体/用户)使用的。理论上,调用者是不清楚API 内部的实现细节的。因此,对公开方法的单测能够保证其结果符合实现预期。同时,单测可以提供给调用者参考使用。
依赖外部输入的方法,需要确定是否处理了对应的异常,已经异步操作是否符合预期。如网络操作,我们需要考虑在网络异常造成的种种问题,通过单测来覆盖异常情况。同时,对于这些异步操作,可以轻松构造不同的input 来检查。
工具方法(until method)是常用方法,通常为 pure function,因此工具方法最适合也最需要用单测来覆盖。
不测什么
- 构造方法
- 私有方法
构造方法无需测试的原因是,构造函数本身不没有其他行为,仅仅将外部传入的参数进行赋值。除此外,单元测试应该和实现细节剥离,而构造函数本身我们应该清楚就是生成实例对象,因此无需测试构造方法。
单测本身重视的是结果,不是实现细节。因此,私有方法本身对外是封闭的,不可知的。在不知道函数定义和内部实现的时候,是无法编写单元测试的(这点肯定有争议)。
单元测试 怎么测
编写
- 分析函数行为
- 抽离外部依赖(mockstub spy)
- 编写用例,判断是否符合逾期
后续维护
- Find the test or tests that define the current behavior that you want to change. 找到测试方法是否已经不符合现在的逾期
- Modify those tests to expect the new desired behavior.修改为当下符合预期
- Run the tests and see if those modified tests fail. 测试,此时应该全部无法通过测试
- Update your code to make all tests pass again. 修改方法,通过测试
从单元测试能获得什么
- 提高代码质量
- 提供重构依据
- 提供用例
延伸 – 单测为什么难写
在写单测的时候,很多时候无从下手,原因是方法实现过于复杂,方法定义没有说明方法预期行为
1 | - (void)updateRepaymentAccordingLastMonthBill:(BOOL)isUpdate card:(Card *)card |
根据注释,这个方法根据传入的卡片和是否更新月账单来更新。 因此测试的时候,我们需要构造isUpdateLastestMonthBill
就可以判断是行为了。
但是
- 这个方法返回是 void,那么是否就智能根据
aCardVo
的属性来判断?如果这样,作为引用传参,aCardVo
就被方法内部改变了 - CardVo 是个很复杂的类,在这里有什么要求?
再通过观察方法实现后发现
- 定义了
isUpdate
,这点依赖无法在外部构造,因此无法进行测试 - 依赖了
isUpdate
为NO 的时候直接返回了,这一点无法在方法定义上提现 - 更新还款状态,进行了数据库操作,但没有判断数据库操作行为,无法判断最终行为结果
综上,这个方法定义,如果根据原则,仅从方法定义上进行编写测试用例,是无法覆盖的。
因此,在遇到这些方法的时候,我们应该先抽离依赖,定义新的方法名,对新定义的方法名进行测试,然后将老的代码进行迁移。最后通过我们编写的测试。
用例
好的单测,本身是就是一个demo,一个说明,应该发生什么,不该发生什么。
测试用例本身就是具体业务的体现。如果业务过于复杂无法控制粒度,那么要如何在接口设计甚至整个业务模块上去做抽象拆分。