原创 | 使用 JUnit、AssertJ 和 Mockito 编写单元测试和实践 TDD (七)CORRECT 边界条件
上一章讲了“对一个工作单元需要测试内容:Right-BICEP”,这一章我们讲讲“CORRECT 边界条件”。
代码中的许多 bug 都出现在“边界条件”附近,也就是说,在那些条件下,代码的行为可能不同于平常的、每天都能运行到的程序路径。
在面向对象的编程中,对象的方法执行结果是对象内部字段和方法参数的函数。我们需要通过单元测试来确认,当字段和/或参数处于边界条件时,方法的执行结果符合我们的预期。
我们用首字母缩略词 CORRECT 来帮助你列举需要测试的边界条件(字段或参数的取值):
Conformance(一致性)——值是否符合预期的格式?
Ordering(有序性)——一组值应该是有序的,还是无序的?
Range(区间性)——值是否在一个合理的区间之内?
Reference(耦合性)——代码是否引用了不受控的外部因素?
Existence(存在性)——值是否为空?
Cardinality(基数性)——是恰好有足够的值?
Time(时间性)——所有的事情是否都是按顺序发生的?是否在正确的时间?是否及时?
你需要好好回答的问题是:
还有什么可能出错?
并针对每个可能出错的地方编写一个测试。
1. Conformance 一致性
值格式是否符合要求?
例如,我们有个方法会从方法参数接收一个字符串形式的 email 地址,并随后向这个 email 地址发送一封标准化邮件。但是用户代码可能在调用这个方法时传入一个不包含“@”的字符串。如果实现代码中没有针对这种情况预先做好应对措施,就有可能在运行中出错。因此,应当针对这类情况预先编写单元测试,检验在传入不能转换成 email 地址的字符串时,方法的响应符合预期。
当然针对上面这个例子,更好的设计是设计一个 EmailAddress 类,并使用它作为方法的参数类型。这样就可以确保只有合法的 email 才能传递给方法,从而不需要针对这个方面编写单元测试。
2. Ordering 有序性
数据或调用是否遵循一定的顺序?
例如以下情况要测试顺序:
如果方法返回一个列表,列表中的元素是否确实按照我们设定的顺序排序?
如果方法执行中需要调用两个或两个以上不同的外部依赖(例如:先写入数据库,再写入缓存),调用时是否确实遵循我们要求的先后顺序?
3. Range 区间性
值是否位于合理的区间之内?
例如:
方法参数充值金额是不是正数?
两个方法参数,fromTime 是否早于 toTime?
数组的索引值是否超出范围?
这些都是需要进行单元测试检测的地方。
4. Reference 耦合性
如果被测工作单元有外部依赖(其他协作类、数据库、文件系统等等)或环境依赖,工作单元的执行结果依赖于这些外部依赖的存在以及它们的状态,那么就应该针对这些情况编写单元测试,保证在这些依赖未满足的情况下运行良好。
主要测试这些方面:
外部依赖存在/不存在,状态正常/异常时,代码的行为是否符合预期?
测试完成后,代码是否像预期的那样调用了这些外部依赖(例如充值成功后是否更新了缓存中的余额)?
5. Existence 存在性
对于你传入或维护的值,先询问自己如果值不存在——如果它为 null, 空集合,或者等于 0,方法的行为将会怎样?
针对上面各种“值不存在”的情况分别编写单元测试。确保你的产品代码在值不存在的情况下的响应符合预期。
6. Cardinality 基数性
在《Pragmatic Unit Testing: In Java with JUnit》一书中举了这样一个例子:假设你正在维护一个披萨店的十大最受欢迎的食品列表。每次有新订单下来,这个列表都会进行自动调整,并将更新后的列表发送到老板的手机上。此时,你需要测试哪些内容呢?
当列表条目不足 10 个时,能出报表吗?
当列表空无一物时,能出报表吗?
当列表只有 1 个条目时,能出报表吗?
当列表条目不足 10 个时,能添加新条目吗?
当列表空无一物时,能添加新条目吗?
当列表只有 1 个条目时,能添加新条目吗?
要是菜单本身就没有 10 个条目,怎么办?
要是菜单中没有任何条目,又怎么办?
这些都是和数量相关的问题。在大多数情况下,你只需要考虑下列三种值:
0
1
n ( n > 1)
这称为 0-1-n 原则。
7. Time 时间性
你需要始终记得以下这些与时间相关的方面:
相对时间(时间上的顺序)
绝对时间(消耗的时间和钟表上的时刻)
并发问题
7.1 相对时间
一些对象会有自己的内部状态。为了维护这些内部状态,你期望 login()会在 logout()之前被调用,prepareStatement()会在 executeStatement()之前被调用,connect()在 read()之前被调用,而 read()又在 close()之前被调用,等等。
你应该编写单元测试,测试如果这些顺序条件不满足时,工作单元的响应是否符合预期。例如,在没有调用 login()之前执行 logout()。如果你的设计意图是未 login()之前执行 logout()会抛出 LogoutBeforeLoginException 异常:
那么你应该写这样的单元测试:
相对时间还包括代码中的超时问题。你需要编写单元测试验证可能的超时出错问题。
7.2 绝对时间
与绝对时间/时刻相关的典型问题是这样一个问题。如果你在实现一个时间工具类。有一个静态方法是计算某个日期加若干个月之后是个什么日期:
如果原始日期是 2020 年 1 月 5 日,我们期待这个方法加 1 个月后是 2020 年 2 月 5 日,加 2 个月后是 2020 年 3 月 5 日。但是如果原始日期是 2020 年 1 月 30 日,那么这个日期加 1 个月是个什么日期?2020 年 2 月 30 日?没有这个日期!每年的 2 月一般只有 20 日,最长也只有 29 日。在这种情况下,我们必须首先定义好 2020 年 1 月 30 日加 1 个月是个什么日期,然后针对这个特殊点编写单元测试。
7.3 并发问题
时间带来的最棘手的问题是并发访问和同步访问的问题。在设计和编写代码的时候,要时刻询问自己:要是多个线程同时访问同一个对象,会出什么问题?然后针对这些可能的问题编写单元测试。
这一章我们讲到这里,下一章将讲讲“好单元测试的特质”!
版权声明: 本文为 InfoQ 作者【编程道与术】的原创文章。
原文链接:【http://xie.infoq.cn/article/913c93b945efba11a346fe297】。文章转载请联系作者。
评论