你的单元测试失败了,而你却不知道为什么?
在软件开发人员抱怨单元测试的众多原因中,处理“不和谐”的测试套件是最大的原因之一。而且,一个软件存在的时间越长,干扰就越大。
澄清一下,我所说的“干扰”指的是那些不断失败的测试,但你知道(认为)它反正是好的,所以你就任其发展。或者是那些有时失败有时成功的测试,但从来没有人去弄清楚或修复它们。还有一些测试是合法失败的,因为代码发生了变化,测试需要更新。所有这些干扰都在呼唤我们的注意,但问题是,干扰越多,我们就越不可能做任何有意义的事情。
但你猜怎么着?在那些“失败但还可以”的测试干扰中,有一些真正的问题,你希望你知道。想想看,就像尝试使用拼写检查器。如果你不跟上它,你会得到各种你不关心的东西,比如特殊的行业单词、名称等,这些都不是真正的拼写问题。但在这些乱七八糟的东西中,藏着的是你真正犯过的尴尬错误——愚蠢的错别字,你想把它们删掉。当然,全球范围内有大量的错误拼写--但与你的软件不同,那里没有太多的固有风险,只是有一点尴尬。
然而,单元测试套件也普遍处于这种状态。很多“不和谐”的结果,我们习惯于看到和忽略,不幸的是,隐藏了我们需要知道和了解的真实结果。在很多组织中,为了解决这个问题,有人每隔一段时间就会安排一次冲刺来清理测试套件,从相隔几个月到甚至几年。花费了大量的时间让测试套件尽可能的干净,但不可避免的是问题又回来了——而且比你预想的更快。这就形成了一个负反馈循环——没有人愿意清理测试,因为他们认为下次测试会再次出现干扰。
答案是采取一种更实用的方法——一种从一开始就消除繁琐、无用的清理冲刺并避免“不和谐”的测试套件的方法。
尽量减少“不和谐”的测试套件
为此,了解单元测试失败意味着什么很重要。归结为三个原因,并有简单的修复方法:
- 代码坏了。所以,修复代码。(这最好是干净的测试套件告诉你的。)
- 代码被适当修改了,现在测试坏了。所以,修复测试以匹配新的代码。(如果你的代码在变化,你可以预期这种情况会发生。一个强有力的理由,当你在研究代码的时候,就应该在测试上下功夫。)
- 测试是错的,而代码是好的。所以,修复测试。(也可能是删除它。但关键是——不要忽略测试。)
现在。你可能会想,如果我的一大堆测试用例都符合那第三类呢?这有什么用呢?所以我们来分析一下。
干扰的原因通常归结为几个基本问题:糟糕的测试、脆弱的测试或糟糕的断言。糟糕的测试是指那些没有做好自己工作的测试。要么是它们的测试比应该的多,要么是它们挂在不一致的数据上,或者是根据外部条件会发生变化。
为了最大限度地减少干扰,确保对于每个给你带来问题的测试(或者更好的是你的所有测试),你对这两个简单的问题有一个好的答案:
- 如果测试通过意味着什么?
- 如果测试失败意味着什么?
如果对于任何测试,你对这两个问题都没有一个合理的答案,那么它就需要改进。
改善脆弱的测试
脆弱的测试是那些容易被破坏的测试。同样,这通常是懒惰断言的症状——仅仅检查东西,因为它们可以被检查,并不意味着它们应该被检查。每个断言都应该与被测试的代码所满足的需求有关,具有真正的意义。常见的罪魁祸首包括日期/时间敏感断言、操作系统依赖性、文件名/路径依赖性、第三方软件安装、合作伙伴API等。确保你只断言你最低限度需要的东西,以便有一个好的测试,并确保你将需要的一切测试是你的源代码控制和构建系统的一部分。
其他不好的断言是指那些不断处于失败状态,但你无论如何也不介意释放的断言(“Oh,thooose are ok,不用担心”),或者是那些不断变化的断言(“之前还好好的,昨天还失败了,今天就好了!!”)。如果代码处于不断变化的状态,短时间内结果不断变化可能是可以的,但长期来看,应该是不能接受的。你需要理解为什么测试结果一直在变化,或者当然也要理解为什么你认为失败了还可以发布。对你的单元测试进行同行评审,包括断言,会对永久解决这个问题有很大帮助。(同行评审的一个额外好处?如果你身处合规环境中,测试是强制监督的一部分,那么生存起来就容易多了。)
评估破损的测试确实是进行大部分清理工作的好地方。我想挑战一下你,认真审视那些已经失败了几个月甚至几年的测试。问问自己,它们是否真的在增加价值。请记住,无论如何你都会忽略这些结果,所以说实话,它们有什么用?去掉你忽略的测试会让你腾出时间来专注于重要的测试,并实际提高你的整体质量。
于是,这就变得相当简单了(虽然可能需要最初的时间投入)。要清理,只需遵守以下最佳实践:
- 定期运行测试,这样它们就不会过时——与代码一起工作。
- 删除那些总是失败的测试,或者修复它们。
- 删除那些不断翻转状态(通过/失败)的测试,或者收紧它们。
- 对单元测试进行同行评审。
当然,不要忘了使用自动化来完成繁琐的工作,这样你花在编写测试上的时间就会更有成效,让你可以创建干扰更小的测试。
使用测试自动化
利用自动化软件测试有助于使单元测试任务不那么繁琐。如果你能让自动化完成简单繁琐的部分(计算机擅长的),就能让你腾出时间来做那些需要实际人类智慧的事情(你擅长的)。例如,让自动化创建你的xUnit测试用例的第一个工作关口(简单的代码,做起来会非常繁琐)。如果你让一个工具自动生成你的getter/setter测试方法,你可以节省大量的时间,你可以用来做其他更有趣的事情。
当我们在测试自动化方面变得更加复杂时,工具可以提供更多的帮助,完成单元测试中一些比较棘手的部分,例如创建和配置存根和mocks。你越是利用自动化的优势,单元测试所花费的时间就越少--而且也会减少很多无聊的时间。如果你使用java,可以看看有个新的单元测试助手,集成在Parasoft Jtest中。它可以做到所有这些事情,还有更多功能,使单元测试不仅更容易,而且更有趣。