如何大幅提高代码覆盖率?


21

我的任务是让遗留应用程序处于单元测试之下。首先介绍一下应用程序的背景知识:这是一个600k LOC Java RCP代码库,存在以下主要问题

  • 大量代码重复
  • 没有封装,大多数私有数据都可以从外部访问,一些业务数据也成为单例,因此不仅可以从外部更改,而且可以从任何地方更改。
  • 没有抽象(例如,没有业务模型,业务数据存储在Object []和double [] []中),因此没有OO。

有一个很好的回归测试套件,一个高效的质量检查团队正在测试和发现错误。我知道如何从经典书籍(例如Michael Feathers)中对其进行测试的技术,但这太慢了。由于存在工作正常的回归测试系统,因此我不怕积极地重构系统以允许编写单元测试。

我应该如何着手解决问题以快速获得覆盖范围,以便能够向管理人员展示进度(实际上是从JUnit测试的安全网中开始赚钱)?我不想使用工具来生成回归测试套件,例如AgitarOne,因为这些测试不会测试是否正确。


为什么不自动创建回归测试,并分别验证每个测试?总比手工编写要快。
罗伯特·哈维

调用任何用Java传统编写的东西听起来有点可笑,但是同意,这肯定是传统。您提到您不害怕重构系统以允许编写单元测试,但是您不应该在尝试任何重构之前按原样在系统上编写单元测试吗?然后您的重构可以通过相同的单元测试来运行,以确保没有任何损坏?
dodgy_coder

1
@dodgy_coder通常都同意,但是我希望有效运作的传统质量检查能使我安全一些。
彼得·科夫勒

1
@dodgy_coder“有效使用旧版代码”的作者Michael C. Feathers将旧版代码定义为“没有测试的代码”。它是一个有用的定义。
StuperUser 2011年

Answers:


10

我相信,在引入单元测试时,可以沿着两个主轴放置代码:A)代码的可测试性如何?B)它的稳定性如何(即它需要多紧急的测试)?仅看极端,这产生了四个类别:

  1. 易于测试且脆弱的代码
  2. 易于测试且稳定的代码
  3. 难以测试且脆弱的代码
  4. 难以测试且稳定的代码

类别1是显而易见的起点,您只需花费较少的时间就可以得到很多好处。类别2使您可以快速改善覆盖率统计(有益于士气)并获得代码库的更多经验,而类别3使工作(通常令人沮丧)更多,但也带来更多收益。您首先应该做什么取决于士气和覆盖范围统计数据对您的重要性。类别4可能不值得打扰。


优秀。我有一个想法,如何确定是否容易通过静态分析进行检查,例如依赖项计数/可测试性资源管理器。但是,如何确定代码是否脆弱?我无法将缺陷与特定单元(例如班级)进行匹配,当然也不能将缺陷排在第三位(上帝班级/单节)。那么也许签到数(热点)?
彼得·科夫勒

1
@Peter Kofler:提交热点是一个好主意,但是这种知识的最有价值的来源将是使用该代码的开发人员。
Michael Borgwardt

1
@Peter-就像Michael所说的那样,使用该代码的开发人员。任何在大型代码库中使用了相当长一段时间的人都将知道它的哪些部分有气味。或者,如果整个事情的气味,它的哪些部分真正的臭气
Carson63000

15

我有很多在遗留系统(虽然不是Java)上工作的经验,比这要大得多。我讨厌成为坏消息的承担者,您的问题就是问题的严重性。我怀疑您低估了它。

向旧代码添加回归测试是一个缓慢,昂贵的过程。许多要求没有得到很好的文档记录-这里的错误修复,那里的补丁,在您不知不觉中,软件定义了它自己的行为。没有测试意味着要实现的全部内容,没有测试可以“挑战”代码中实现的隐式需求。

如果您尝试快速获得覆盖,则可能会急着完成工作,将其烘烤一半,然后失败。测试将部分覆盖明显的内容,而对实际问题的覆盖范围很小甚至没有。您会说服您想要出售给单元测试部门的经理,这是不值得的,因为这只是另一条无法解决的问题。

恕我直言,最好的方法是针对您的测试。使用指标,直觉和缺陷日志报告来确定产生最多问题的代码的1%或10%。重击这些模块,而忽略其余模块。不要尝试做太多,少即是多。

一个现实的目标是“自从我们实施UT以来,被测模块中的缺陷插入率已下降到不属于UT的那些模块的x%”(理想情况下,x为小于100的数字)。


+1,如果没有比代码强的标准,就无法有效地对某项进行单元测试。
dan_waterworth 2011年

我知道并且同意。区别在于我们有测试,传统的回归测试是通过进行质量检查来实现的,因此存在某种安全网。其次,我非常支持单元测试,因此绝对不会是另一件事。关于首先要定位的目标的一个好点。谢谢。
彼得·科夫勒

1
而且不要忘记,仅仅针对“覆盖率”并不会提高质量,因为您将陷入一堆充满缺陷和琐碎的测试中(以及不需要显式测试的琐碎代码测试,但是添加只是为了增加覆盖范围)。您最终将创建测试来取悦覆盖率工具,而不是因为它们很有用,并且可能会更改代码本身以增加测试覆盖率而无需编写测试(例如删除注释和变量定义,某些覆盖率工具将调用未发现的代码)。
jwenting 2011年

2

我想起了那句话,那就是当马已经拴好时,不用担心谷仓门。

现实情况是,确实没有一种经济有效的方法来获得旧系统的良好测试覆盖率,当然这并不是在很短的时间内完成的。正如MattNz提到的那样,这将是一个非常耗时的过程,并且最终成本很高。我的直觉告诉我,如果您尝试做一些事情来打动管理人员,您可能会造成新的维护噩梦,因为它试图在没有完全理解要测试的要求的情况下显示得太快太快。

实际上,一旦您已经编写了代码,就有效地编写测试几乎已经到了后期,而没有丢失重要内容的风险。另一方面,您可以说某些测试总比没有测试要好,但是如果是这样,则测试本身需要表明它们为整个系统增加了价值。

我的建议是着眼于那些您认为某些东西“破损”的关键区域。我的意思是,这可能是效率极低的代码,或者您可以证明以前的维护成本很高。记录问题,然后以此为出发点,引入可以帮助您改进系统的测试级别,而无需进行大量的重新设计工作。这里的想法是避免赶上测试的步伐,而是引入测试来帮助您解决特定的问题。一段时间后,请查看您是否可以衡量和区分维护该部分代码的先前成本,以及当前对采用其支持测试的修补程序所做的努力。

要记住的是,管理层对成本/收益及其对客户的直接影响以及最终对预算的影响更感兴趣。他们永远不会仅仅因为这是最好的事情而对做某事感兴趣,除非您能证明它会为他们带来他们所感兴趣的利益。如果能够证明您正在改进系统并获得对当前工作的良好测试覆盖率,则管理层更有可能将其视为您所做工作的有效应用。这可能使您可以争论将您的工作扩展到其他关键领域,而又不要求完全冻结产品的开发,甚至不要求进行重写几乎是不可能的!


1

提高覆盖率的一种方法是编写更多测试。

另一种方法是减少代码中的冗余,以使现有测试实际上覆盖当前未涵盖的冗余代码。

假设您有3个代码块,a,b和b',其中b'是B的重复副本(精确或接近未命中副本),并且测试T覆盖了a和b,但没有覆盖b'。

如果通过从b和b'作为b提取公共性来重构代码库以消除b',则代码库现在看起来像a,b0,B,b'0,其中b0包含b'0的非共享代码,反之,反之亦然,并且b0和b'0都比B小得多,并且调用/使用B。

现在程序的功能没有改变,也没有测试T,因此我们可以再次运行T并期望它通过。现在覆盖的代码是a,b0和B,但不是b'0。代码库变得越来越小(b'0小于b'!!),我们仍然覆盖了我们最初介绍的内容。我们的覆盖率提高了。

为此,您需要找到b,b',理想情况下,找到B才能进行重构。我们的CloneDR工具可以针对多种语言(尤其是Java)执行此操作。您说您的代码库中有很多重复的代码。这可能是解决您的问题的好方法。

奇怪的是,发现b和b'的行为通常会增加您对该程序实现的抽象概念的词汇量。尽管该工具不知道b和b'的功能,但将它们与代码隔离的行为使他们仅关注b和b'的内容,常常使程序员对克隆的代码实现抽象B_abstract有一个很好的了解。 。因此,这也提高了您对代码的理解。请确保在表扬您的名字时给它一个好名字,这样您将获得更好的测试覆盖范围和更易维护的程序。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.