小剑

单元测试思考

长期以来基本是围绕遗留项目开发业务为主,单元测试很难实行。分析原因大致是下面几个:

  1. 时间紧,来不及写
  2. 模块纠缠不清,写不出
  3. 依赖好多环境,测试很难跑

终于在一次新项目中强迫自己实行后,有了一些总结。

写可测试的代码

测试难写的原因在于写代码时没有为测试考虑,更准确的说是没有设计好你写的代码,设计良好的代码一定是方便测试的。所以,如果有心实行单元测试的同学,记住,写的时候考虑一点,能写出对应的测试吗?(这应该也是TDD出现的原因吧,测试先行强制了代码的可测试性)

那么怎么围绕测试去设计代码呢?我的一个思路是起名字,因为当你给一堆代码命名之后,这段代码就有了定义,他的作用是什么,需要什么入参,会返回什么都是趋于清晰的。着手一项开发任务时,你要做的是拆解这个任务到可以由各种名字组合出来,然后验证各个拆出来的名字是不是方便写测试,如果还是不方便,继续拆解这个名字

在我看来起名字其实就是模块化,只是原来理解的模块更多的是更加业务功能划分分块,经历单元测试之痛后,我给模块化定了一个明确的标准:可测试。

当每个配上了足够多测试的模块组合后,整体的质量也是更有保障的。

环境依赖问题的思路

实践中还会碰到环境依赖问题,一个典型的例子就是数据库。这个根据实际情况大致有三种方式应对:

  1. mock外部依赖。伪造外部依赖,不依赖真实依赖环境。
  2. 拆出来,可以脱离依赖单独测。如果测试点不在依赖上,推荐使用这个方式。
  3. 人为构造适合测试的外部依赖环境。以数据库为例,就是执行前初始化一些数据,执行完后库回滚,很多测试框架都支持。

举个例子,一个借贷系统中的还款功能,原始版本是计算和更新库黏在一起的,因为校验点在更新后的每期还款数据,所以只能选择方式3,不同的测试用例需要初始化不同的数据,查库校验,然后回滚。

还款(借款ID, 控制参数) {
    每期还款数据[] = 查询未还清记录()
    组合还款数据和参数计算并更新库()
}

另个思路结合了起名字法和方法2,拆出还款计算器,还款计算器内只是纯计算,测试时不依赖运行的数据库,构造不同情况的每期还款数据做为入参,校验返回值即可。而还款方法本身则只要保证更新库的正确即可,不同情况还款的多样性转嫁到了方便测试的还款计算器上了。

还款(借款ID, 控制参数) {
    还款计算器()
    更新库()
}

还款计算器(每期还款数据[], 控制参数) {
    还款后每期还款数据[] = 组合还款数据和控制参数计算()
    返回 还款后每期还款数据[]
}

单元测试的基本点

看到好多小伙伴写的测试没有校验,只是输出然后通过人眼查看是否正确。正确的方式是通过代码去校验,断言返回值是否符合预期,这样每次回归时才有效率,而不是人眼一个个看。

另一个点是测试用例需要随着每次代码的调整去同步。当你增加功能了,需要补充测试用例;当你修复了一个bug,需要补充造成该bug的测试用例。

最后

关于文首说到的没时间写测试,这个就像是道理我都懂,就是做不到。需要强迫自己去实行几次,慢慢建立起习惯后,会发现写测试的时间大部分情况还都是有的。