您的位置:首页 > 其它

原创 | 使用 JUnit、AssertJ 和 Mockito 编写单元测试和实践 TDD (七)CORRECT 边界条件

2020-05-15 17:44 218 查看



上一章讲了“对一个工作单元需要测试内容: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异常:

if (!loggedIn) {
throw new LogoutBeforeLoginException();
}

那么你应该写这样的单元测试:

assertThrows(LogoutBeforeLoginException.class, () -> {
instance.logout();
});

相对时间还包括代码中的超时问题。你需要编写单元测试验证可能的超时出错问题。

7.2 绝对时间

与绝对时间/时刻相关的典型问题是这样一个问题。如果你在实现一个时间工具类。有一个静态方法是计算某个日期加若干个月之后是个什么日期:

public static LocalDate addMonths(LocalDate origDate, int months)

如果原始日期是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 并发问题

时间带来的最棘手的问题是并发访问和同步访问的问题。在设计和编写代码的时候,要时刻询问自己:要是多个线程同时访问同一个对象,会出什么问题?然后针对这些可能的问题编写单元测试。

这一章我们讲到这里,下一章将讲讲“好单元测试的特质”!


-THE END-
原创作者 | 杨宇Yangyu
编程道与术原创内容
​转载请注明“编程道与术”出处

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐