65.9K
CodeProject 正在变化。 阅读更多。
Home

单元测试套件质量估算

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (9投票s)

2019年5月6日

BSD

9分钟阅读

viewsIcon

15551

downloadIcon

109

一种简单的方法来估算单元测试套件的质量,这种方法可以提供比常规代码覆盖率更深入的见解。

引言

本文介绍了一种简单的方法来估算单元测试套件的质量,这种方法可以提供比常规代码覆盖率更深入的见解。

代码覆盖率问题

想象一下一个开发团队正在开发一个软件产品。他们编写代码和单元测试。他们可以展示超过 50% 的代码覆盖率,但他们对任何更改都异常谨慎,总是试图减轻风险。这似乎很奇怪,因为 50% 以上的覆盖率对于应用程序来说已经相当不错了(对于可重用库来说则不然),并且在代码更改时应该提供相当大的信心。但在这个案例中并非如此,因为问题不在于数量,而在于测试的质量。

以已执行语句百分比表示的代码覆盖率的主要问题在于,它并不等于以已执行用例数量衡量的覆盖率。它所衡量的只是已执行代码行的数量,而没有考虑到实际是否进行了任何验证。这限制了此指标单独的效用。

质量定义

高质量的测试套件应该提供关于被测代码行为的高度信心。它最好能编码所有已交付功能的所有必需行为(以用例表示),以便自动检测到所有不期望的行为。这是完美的境界。本文的其余部分将介绍帮助监控测试套件质量的扩展指标。

示例

为了说明所介绍的方法,本文将贯穿使用以下被测代码。该类生成奇数。布尔属性指示生成器仅发出素数。

public final class NumberGenerator {

	private boolean primeNumbersOnly;

	public NumberGenerator() {

		this.primeNumbersOnly = false;
	}
	public boolean isPrimeNumbersOnly() {

		return primeNumbersOnly;
	}
	public void setPrimeNumbersOnly(boolean primeNumbersOnly) {

		this.primeNumbersOnly = primeNumbersOnly;
	}

	public List<Integer> generateOdd() {

		List<Integer> result = new ArrayList<>();

		for (int i = 1; i < 10; i += 2) {
			if (this.primeNumbersOnly) {
				if (isPrime(i)) {
					result.add(i);
				}
			} else {
				result.add(i);
			}
		}

		return result;
	}

	private static boolean isPrime(int x) {

		for (int i = 2; i < x; i++) {
			if (x % i == 0) {
				return false;
			}
		}
		return true;
	}
}

此外,除了被测代码,本文还将贯穿使用以下单元测试套件。

public class NumberGeneratorTest {
	
	@Test
	public void testConstruction1() {
		
		NumberGenerator ng = new NumberGenerator();
		assertNotNull(ng);
	}
	@Test
	public void testConstruction2() {
		
		try {
			NumberGenerator ng = new NumberGenerator();
		} catch(Exception e) {
			fail();	  
		}
	}
	
	@Test
	public void testPrimeNumbersOnly() {
		
		NumberGenerator ng = new NumberGenerator();
		ng.setPrimeNumbersOnly(true);
		assertTrue(ng.isPrimeNumbersOnly());
	}
	
	@Test
	public void testGenerateOdd1() {
		
		NumberGenerator ng = new NumberGenerator();
		ng.setPrimeNumbersOnly(false);
		
		List<Integer> result = ng.generateOdd();
		assertEquals(Arrays.asList(1,3,5,7,9), result);
	}
	
	@Test
	public void testGenerateOdd2() {
		
		NumberGenerator ng = new NumberGenerator();
		ng.setPrimeNumbersOnly(false);
		
		List<Integer> result = ng.generateOdd();
		assertEquals(Arrays.asList(1,3,5,7,9), result);
		
		ng.setPrimeNumbersOnly(true);
		
		result = ng.generateOdd();
		assertEquals(Arrays.asList(1,3,5,7), result);
	}
}

质量估算

为了确定测试套件的质量,需要审查测试用例和被测代码。最好审查整个测试套件,但为了估算一个现有的大型测试套件,应该收集足够的样本来推理整个代码库(至少 100 个)。在粗略检查期间,每个测试用例都会被分配三个数值:验证率、用例覆盖率和覆盖的复杂性。

验证率

 验证率表示测试是否实际验证了任何内容。值为 1 分配给验证了已执行代码行为的测试(testPrimeNumbersOnlytestGenerateOdd1testGenerateOdd1),值为 0 分配给仅运行代码的测试(testConstruction1testConstruction1)。可能存在乍一看并不明显的情况。在这种情况下,可以分配任意分数(请记住,这只是估算)。

用例覆盖率

用例覆盖率表示测试应覆盖的所有用例数量与实际执行的用例数量之比。此参数很棘手,因为它需要查看整个套件(通常是整个源文件)来找到被检查功能的​​所有已执行用例。例如,如果被测函数接受一个布尔参数,则用例数量为 2(一个用于“true”值,一个用于“false”)。如果测试同时执行两者(testGenerateOdd2),则此参数应为 1。如果测试仅执行其中一个(testGenerateOdd1),则应分配 0.5,除非有另一个测试用例处理了另一个值,那么两个用例都应分配 1。只有当验证率为 0 时,才应分配 0,因为不验证任何内容的测试也不覆盖任何用例(testConstruction1testConstruction2)。由于这是估算,如果仅执行了“阳光下的场景”(sunny day scenario),则可以分配 0.5 的值(那么平均误差仅为 0.25)。

复杂性影响

复杂性影响是粗略估算,用于判断被测代码是否包含值得测试的功能。对于包含算法(条件、循环或函数调用序列)的常规代码,测试用例可以分配值 1testGenerateOdd2)。对于执行 getter 和 setter 以及仅复制变量的其他函数,测试用例应分配值 0.1testConstruction1testConstruction2testPrimeNumbersOnly)。在极少数情况下,如果难以确定测试是否实际运行了被测代码(sic!)或者测试是冗余的(testGenerateOdd1 因为它是 testGenerateOdd2 的子集),则可能分配 0。此参数本质上非常粗糙,因为在 0.11 之间有很大的自由度。

结果

代码审查的结果应该是一个包含 5 列并分配了值的表。

表 1. 审查结果

测试名称 验证率 用例覆盖率 复杂性影响 注释
testConstruction1 0 0 0.1 不验证任何内容
testConstruction2 0 0 0.1 不验证任何内容
testPrimeNumbersOnly 1 0.5 0.1  
testGenerateOdd1 1 0.5 0 冗余
testGenerateOdd2 1 1 1  

平均覆盖复杂性 (ACC)

平均覆盖复杂性是通过所有被检查测试用例的所有三个参数的乘积的平均值来计算的。

ACC = AVERAGE(验证率(i) * 用例覆盖率(i) * 复杂性影响(i))
其中 i = 1 到被审查的测试数量。

下表展示了底部右侧单元格(值为 0.21)的示例套件的值。

表 2. 测试套件质量指标

测试名称 验证率 用例覆盖率 复杂性影响 注释 覆盖复杂性
testConstruction1 0 0 0.1 不验证任何内容 0
testConstruction2 0 0 0.1 不验证任何内容 0
testPrimeNumbersOnly 1 0.5 0.1   0.05
testGenerateOdd1 1 0.5 0 冗余 0
testGenerateOdd2 1 1 1   1
平均值 0.6 0.4 0.26   0.21

接近 1 的值意味着测试套件专注于复杂且非平凡的功能。这样的测试套件为软件团队提供了信心。另一方面,接近 0 的值表明套件没有太多验证,专注于平凡的功能,或者没有完全执行所有可能的用例(或它们的所有组合)。

代码覆盖率和平均覆盖复杂性相结合,可以洞察测试套件的质量,如 1 图所示。

图 1. 测试套件质量。

图表的解释如下

  • 两个参数都大于 0.5 或 50%(绿色象限)表示测试套件涵盖了被测代码的大部分相关复杂用例;这是一个理想的情况。
  • 代码覆盖率低于 50%,平均覆盖复杂性高于 0.5(黄色象限)表示测试套件质量良好但数量不足(许多功能尚未完全覆盖,但已覆盖的功能已得到彻底执行);这种情况发生在测试在开发过程中被放弃,或者是在事后编写的,通常是重构的前提;为了将套件移至绿色象限,需要编写更多测试来覆盖剩余的功能部分。
  • 代码覆盖率高于 50%,平均覆盖复杂性低于 0.5(红色象限)表示一个“欺骗性”的测试套件,它仅用于欺骗代码覆盖率指标;应删除此类套件。
  • 两个参数都低于 0.5 或 50%(灰色象限)表示测试套件本身的存在是可疑的;应重构或删除此类套件。

完美点是测试套件的指导方向,但要达到它是否可能或值得(通常成本太高)。提供最高投资回报率的测试套件通常位于浅绿色象限内,通常平均覆盖复杂性高于代码覆盖率。在这种情况下,所有非平凡的功能都被测试套件覆盖。

附加指标

为了更深入地了解问题,可以计算其他指标

  • 平均验证率 – 显示多少百分比的测试用例实际验证了某些内容;低值(示例中为 0.61)意味着许多测试用例只是运行被测代码
  • 平均用例覆盖率 – 显示测试执行的用例比例;低值(示例中为 0.4)意味着测试不详尽
  • 平均复杂性影响– 显示覆盖代码的平均验证复杂性;低值(示例中为 0.26)意味着测试套件专注于平凡的功能,这些功能不太可能出错

为提高质量而重构

所描述的方法可用于指导测试套件的重构以提高质量。重构从删除所有验证率为 0 的测试开始,因为它们没有任何目的。然后应删除所有复杂性影响为 0.1 的测试,因为它们的效用很小。执行这两个步骤会导致代码覆盖率下降,显示测试套件未覆盖的功能。之后,所有用例覆盖率小于 1 的测试都应得到改进,并编写新的高质量测试。

自动化

所介绍的方法可以在一定程度上自动化。如果编程语言支持注解,可以开发一个特殊的注解来将分配给每个测试用例的参数与测试用例一起保存,如下面的代码片段所示。

@Test
@Quality(verificationRatio = 1, useCaseCoverage = 0.5, complexityImpact = 0.1, 
	  comment="Tests trivial assignment.")
public void testPrimeNumbersOnly() {
		
	NumberGenerator ng = new NumberGenerator();
	ng.setPrimeNumbersOnly(true);
	assertTrue(ng.isPrimeNumbersOnly());
}

测试用例评估可以作为常规代码审查的一部分,并且可以扩展代码覆盖率工具以在测试套件执行期间计算聚合指标。

注意:一个包含 @Quality 注解和计算所述指标的 Apache Ant 任务的 jar 文件可在此处 找到

快速 ACC 估算

上述方法耗时。为了快速估算平均覆盖复杂性,可以使用变异测试系统(例如 pittest.org)。变异测试涉及以各种小方式修改被测代码。每个变异版本称为一个变异体。然后,在未修改的测试套件下执行每个变异体。如果测试套件失败,则该变异体被杀死,否则该变异体存活,这意味着测试套件无法检测到行为的改变。变异测试系统报告所有存活和被杀死的变异体,如图 2 所示。

图 2. 变异测试报告(pitclipse)。
 
的“存活比”(大于 1)表示 ACC,因为许多用例未被执行。的“存活比”(小于 1,例如 30/168 = 0.178)表示 ACC,这意味着高质量的测试套件(“存活比”在某种程度上与 ACC 相反)。此方法的弱点在于,低的“存活比”没有考虑到测试套件的冗余,因此在极端情况下,由于存在许多冗余测试,ACC 可能实际上更接近 50%。

结论

所介绍的组合指标(代码覆盖率和平均覆盖复杂性)提供了对测试套件质量的一些定性见解,但需要注意的是,平均覆盖复杂性只是一个粗略的估计,可能会有相当大的差异。仅当被检查测试用例集能够代表整个套件时,它才具有代表性,并且取决于检查者对特定测试用例的任意估计。尽管如此,这两个指标的组合比单独的代码覆盖率更能反映测试套件的质量。

本文附加了一个示例电子表格。

历史

  • 2019 年 5 月 5 日:初始版本
© . All rights reserved.