利用自动JUnit代码覆盖率测试用例生成来快速构建和扩展测试

1,247 阅读9分钟

我最近写过一篇关于陷入代码覆盖率百分比陷阱的文章,这引发了热烈的讨论,所以我想我将更深入地探讨代码覆盖率问题和解决方案。具体来说,涵盖覆盖率本身、自动生成的JUnit测试的值以及如何识别有问题的单元测试。另外,还会浅显地提及如何在执行方面继续做得更好。

JUnit代码覆盖率如何工作?

让我们从覆盖率指标本身以及我们如何计算代码覆盖率开始。代码覆盖率数字通常是无意义的,或充其量是会造成误导的。如果你“确实”拥有100%的代码覆盖率,那又意味着什么呢?你是如何测量的?

有很多不同的方法来衡量覆盖率。

衡量代码覆盖率的一种方法是从需求角度出发。你是否对每一项要求都进行了测试?这是一个合理的开始……但这并不意味着所有代码都已经过测试。

衡量代码覆盖率的另一种方法是通过测试的次数(别笑,我实际上是在真实案例中听到的)。真的,我是说真的!这是一个非常糟糕的指标,显然毫无意义。可能是比简单地计算你拥有多少个测试还差?当然,这个我不好说。

然后我们来尝试确定执行了什么代码。常见的覆盖率指标包括语句覆盖率、行覆盖率、分支覆盖率、决策覆盖率、多个条件覆盖率,或者更全面的MC/DC或修改后的条件/决策覆盖率。

当然,最简单的方法是行覆盖率,但是你可能已经看到,工具对此进行了不同的衡量,因此覆盖率将有所不同。执行代码行并不意味着你已经检查了该代码行中可能发生的所有不同情况。这就是为什么安全关键标准(例如用于汽车功能安全的ISO 26262和用于机载系统的DO-178B/C)都需要MC/DC。

这是一个简单的代码示例,假设x,y和z为布尔值:

If ( (x||y) && z) { doSomethingGood(); } else {doSomethingElse();}

在这种情况下,无论我的值是多少,该行都被“覆盖”了。诚然,这是一种将所有内容都放在一行上的草率方法,但是你看到了问题。人们实际上以这种方式编写代码。但是,让我们整理一下。

If ( (x||y) && z) {
doSomethingGood();
} else {
doSomethingElse(); /* because code should never doSomethingBad() */

一目了然,我可能会得出这样的结论:我只需要两个测试——一个将整个表达式评估为TRUE并执行doSomethingGood() (x=true, y=true, z=true),而另一个则评估为FALSE并执行doSomethingElse() (x=false, y=false, z=false)。行覆盖率说我们很高兴,“一切都经过了测试。”

但是请稍等,还可以通过多种方式测试主表达式:

x值y值z值决定值
FalseFalseTrueFalse
FalseTrueTrueTrue
FalseTrueFalseFalse
TrueFalseTrueTrue

这是一个简单的示例,但它说明了这一点。我这里需要进行4个测试才能真正正确地覆盖代码,至少在我关心MC/DC覆盖率的情况下。当我完成一半时,行覆盖率会说100%。我将再一次不再详细说明MC/DC的价值。这里的重点是,无论你使用哪种方法衡量覆盖率,通过断言进行验证的内容都有意义,而且是很重要的

无意义的自动生成

许多人陷入的另一个陷阱是添加了一种不成熟的工具来自动生成单元测试。

简单的测试生成工具可以创建无需任何声明即可执行代码的测试。这样可以避免测试变得嘈杂,实际上也意味着你的应用程序不会崩溃。不幸的是,这并不能告诉你应用程序是否在执行应做的事情,而这却很重要。

新一代工具通过根据它们可以自动捕获的任何特定值创建断言来工作。但是,如果自动生成会产生大量断言,则最终会产生大量麻烦。这里没有中间立场。你要么拥有易于维护但毫无意义的东西,要么拥有值得怀疑的维护“噩梦”

首先,许多自动生成单元测试的开源工具看起来很有价值,因为你的覆盖率实现可以非常快。真正的问题在于维护。通常,在开发过程中,开发人员会付出额外的努力来微调自动生成的断言,以创建他们认为干净的测试套件。但是,断言是脆弱的,不能随着代码的更改而适应。这意味着开发人员在下次发布时必须再次执行大部分“自动”生成。测试套件可以重用。如果你无法重复使用它们,则说明你做错了什么。

这也不能掩盖一个可怕的想法,即在第一次运行时,当你具有较高的覆盖率时,测试中的断言没有应有的意义。仅仅因为可以断言某些东西,并不意味着应该这样做,或者甚至是正确的事情。

public class ListTest {
private List<String> list = new ArrayList<>();
@Test
public void testAdd() {
list.add(“Foo”);
assertNotNull(list);
}
}

理想情况下,断言将检查代码是否正常运行,并且当代码工作不正常时,断言将失败。有很多断言都不是很容易的,我们将在下面进行探讨。

原始覆盖率与有意义的测试

如果你要以覆盖率高、可靠、干净的测试套件为代价来追求高覆盖率的数值,那么你将失去其真正的价值。一套维护良好的测试套件使你对代码充满信心,甚至是快速安全重构的基础。嘈杂和/或毫无意义的测试意味着你不能依赖测试套件,不能重构,甚至不能发布。

人们对代码进行衡量(尤其是根据严格的标准)时,会发现他们发现自己的代码覆盖率比自己想要的要低。通常,这最终导致他们开始盲目的追逐代码覆盖率。让我们拭目以待吧!现在,你可能会因为不合理的信念而陷入危险的境地,即自动化JUnit测试已经创建了有意义的测试,或者通过手工创建了意义不大且维护成本很高的单元测试。

在现实世界中,维护测试套件的持续成本远远超过了创建单元测试的成本,因此从一开始就创建良好的干净单元测试非常重要。你会知道这一点,因为你可以在持续集成(CI)流程中始终运行测试。如果你仅在发行时运行测试,则表明测试噪音大。讽刺的是,这使得测试变得更糟,因为它们没有得到维护。

软件测试自动化还不错,实际上,这是必要的,因为它具有当今常见的复杂性和时间压力。但是,自动生成价值通常比其价值更麻烦。与随意创建断言相比,基于扩展值,监视实际系统以及创建复杂的框架、模拟和存根的自动化提供了更多的价值。

你能做什么?

测量

第一步是测量并获得有关你当前覆盖率的报告,否则,你将不会知道自己的位置以及情况是否会好转。进行此操作时,衡量所有测试活动(包括单元、功能、手册等)并正确汇总覆盖率非常重要。这样,你将尽最大的努力去努力——在完全未经测试的代码上,而不是端到端测试涵盖但没有碰巧的代码上单元测试。Parasoft可以准确地汇总来自多个运行和多种类型测试的代码覆盖率,从而为你提供准确的位置信息。

构架

为你创建单元测试框架的工具是一个很好的入门方法。确保这些工具连接到Mockito和PowerMock等常见的模拟框架,因为实际代码很复杂,并且需要存根和模拟。但这还不够,你需要能够:

  • 创建有意义的模拟
  • 使用更大、更广泛的数据扩展简单的测试
  • 监视正在运行的应用程序

智能协助

你可以手动完成所有这些操作,但是这会花费大量时间和精力。这是利用自动化的绝佳场所——例如,Parasoft Jtest在IDE中实时提供自动建议,并与开源框架(JUnit,Mockito,PowerMock等)集成,以帮助用户创建、扩展和维护他们的JUnit测试套件并提供更广泛的覆盖率。如果你对此技术感到好奇,则可以了解有关程序员为什么讨厌单元测试却又不得不重视它的更多信息。

总结

如果覆盖率对于你的项目来说是一个问题,请确保你对其进行了正确的测量,并从你运行的所有测试中对所有方面进行了衡量。随着你开始使用单元测试扩展覆盖率,你可以利用指导性的测试创建来快速创建和扩展测试,以获取有意义的可维护代码覆盖率。Parasoft Jtest将创建随着代码的增长和更改而可维护的测试,因此你不必一遍又一遍地重复执行相同的工作。