这篇文章梳理一下Java软件测试中的Secification test和Structure test。

Specification Test

规范测试(specification test):又称黑盒测试(black-box testing)或需求驱动测试(requirements-driven testing),这种测试方法关注程序的功能和性能,而不关注其内部实现。

Specification(规范)是对软件组件、系统或方法的详细描述,它定义了预期的功能、行为和性能。通常包括以下内容:

  1. 每个参数:描述方法的输入参数,包括数据类型、取值范围和参数的作用。
  2. 返回值:描述方法的输出结果,包括数据类型、取值范围和返回值的含义。
  3. 每个异常(检查和未检查):列出可能抛出的异常,以及在什么情况下会抛出这些异常。
  4. 方法的功能和行为,包括: a. 主要目的:描述方法的核心功能和作用。 b. 副作用:说明方法在执行过程中可能产生的其他影响或结果。 c. 线程安全性问题:描述方法在多线程环境中的表现和潜在问题。 d. 性能问题:阐述方法的性能特点,如时间复杂度、空间复杂度或其他性能指标。

总之就是除了实现细节之外,specification里基本都有了,这样子我们就可以仅通过这个specification来进行测试,不管其内部是如何实现的。比如在下面的例子中,我们只需要对功能前面的Docstring Specification来进行展开测试即可。

class RepeatingCardOrganizer {
 ...
 /**
 * Checks if the provided card has been answered correctly the required 
number of times.
 * @param card The {@link CardStatus} object to check.
 * @return {@code true} if this card has been answered correctly at least 
{@code this.repetitions} times.
 */
 public boolean isComplete(CardStatus card) {
 // IGNORE THIS WHEN SPECIFICATION TESTING!
 }
}

另外,有一个叫边界测试(Boundary Value Testing)在基于规范的测试(Specification Testing)中是非常重要的,比如说有一个文档写着如果我们拥有的金额大于价格我们就购买成功,否则购买失败,那么这个时候要进行specification testing就必须要把这个boundary也就是金额恰等于价格的边界情况考虑进去,因为实际应用中,许多软件缺陷和错误都是由边界条件引起的,在基于规范的测试中进行边界测试是非常有必要的。

Structural Test

结构测试(structure test):又称白盒测试(white-box testing)或逻辑驱动测试(logic-driven testing),这种测试方法关注软件的实现,优化各种代码覆盖率,如行覆盖(line coverage)、语句覆盖(statement coverage)、数据流覆盖(data-flow coverage)等。测试者需要了解代码的实现细节以设计测试用例。

对于各种不同的覆盖指标,如Statement coverage(又叫line coverage), Branch Coverage, Path coverage等,用下面一个代码例子来说明:

public boolean pay(int cost, boolean useCredit) {
 if (useCredit) {
     if (enoughCredit) {
     return true;
     }
 }
 if (enoughCash) {
     return true;
     }
 return false;
}

1. Statement coverage(语句覆盖/行覆盖)

这个其实最好理解,就是写的测试一共能让多少行代码以及哪些语句被执行。根据被执行语句的覆盖程度来判断测试的完成度。在这里我们只需要写3个测试用例就能全部覆盖上面的语句。它们分别是:

1 useCredit : True, enoughCredit: False, enoughCash: anyValue. 

2 useCredit : False, enoughCredit: anyValue, enoughCash: True.

3 useCredit : False, enoughCredit: anyValue, enoughCash: False.

2. Branch Coverage(分支覆盖)

这种覆盖率指标关注代码中的条件结构(如 if-else 语句和 switch 语句)。分支覆盖度量测试用例是否覆盖了代码中所有可能的条件分支。相较于语句覆盖,分支覆盖提供了更全面的测试质量评估,因为它确保了代码中的每个条件分支都得到了测试。比如在上面的Statement coverage中,当进入到了if (useCredit) 分支后,对于 if (enoughCredit) 不成立的情况我们并没有测试到,虽然程序中没有写else语句,但这一分支是实际存在的,我们同样需要测试到,因此我们要在上面的测试用例基础上再加一条: useCredit : True, enoughCredit: False, enoughCash: True. 这样一共4个测试用例就达到了所有分支branches的覆盖。

3. Path coverage(路径覆盖)

然而,分支覆盖仍然不能保证覆盖所有可能的执行路径。Path coverage(路径覆盖)是一种更高级的代码覆盖率指标,用于评估测试用例是否覆盖了代码中所有可能的执行路径。路径覆盖涉及到所有条件、循环和函数调用的组合。理论上,路径覆盖率可以确保对软件进行了最全面的测试,但在实践中,路径覆盖率可能难以实现,因为复杂的代码可能包含大量的执行路径,使得覆盖所有路径变得不切实际。具体Path coverage都需要哪些测试用例呢?用图来表示最简单,我们根据代码的实现细节,知道一共有以下所有情况:

Java Test: Specification and Structure Testing(line, branch, path coverage)-LMLPHP

要实现100%的路径覆盖,我们只需要从上往下把所有可能的路径都走一遍,你会发现一共有5条不同的路,也就分别对应了5个不同的测试用例,比刚刚又多加了一条,这样一来就完成了100%的路径覆盖Path coverage了。

基于这个例子,对几种不同的代码覆盖率指标的小结图表:

Java Test: Specification and Structure Testing(line, branch, path coverage)-LMLPHP

小结

这篇文章讲了两大类不同的测试方式:specification testing以及structural testing。对于前者讲了一下概念以及当中特别重要的boundary value testing;对于后者也讲了定义外加梳理了一下几种不同的代码覆盖率指标以及例子。

05-05 00:41