本文是作者在项目组内推行单元测试的一些心得总结。
单元测试(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,一个说明,应该发生什么,不该发生什么。
测试用例本身就是具体业务的体现。如果业务过于复杂无法控制粒度,那么要如何在接口设计甚至整个业务模块上去做抽象拆分。