单元测试最佳实践:如何最大程度地利用测试自动化
单元测试是一种众所周知的做法,但是还有很多改进的空间!在这篇文章中,最有效的单元测试最佳实践,包括一路最大化自动化工具的方法。我们还将讨论代码覆盖率、模拟依赖关系和整体测试策略。
什么是单元测试?
单元测试是测试应用程序的单个单元或组件的一种做法,目的是验证每个单元或组件是否正常工作。通常,一个单元应该只占应用程序的一小部分——在Java中,它通常是单个类。请注意,我并不是在这里严格定义“单元”,而是由开发人员来决定每个测试的测试代码范围。
人们有时将“单元测试”与“集成测试”或“端到端测试”相对比。区别在于,通常通过进行单元测试来验证单个可测试单元的行为,而集成测试则是在一起验证多个组件或整个应用程序的行为。就像我说过的那样,对“单元”的定义并没有严格定义,具体取决于每个测试的范围。
为什么要进行单元测试?
单元测试是一种行之有效的技术,可确保软件质量,并带来很多好处。这是(多个)进行单元测试的重要原因:
- 单元测试可以验证您的每款软件不仅可以在今天正常运行,而且可以在将来继续运行,为将来的开发奠定了坚实的基础。
- 单元测试可以在生产过程的早期阶段识别出缺陷,从而降低了在开发周期的后期阶段修复缺陷的成本。
- 单元测试的代码通常更安全地重构,因为可以快速重新运行测试以验证行为没有改变。
- 编写单元测试迫使开发人员考虑设计生产代码以使其适合于单元测试的程度,并使开发人员从不同的角度看待他们的代码,鼓励他们在实现过程中考虑极端情况和错误情况。
- 在代码审查过程中包含单元测试可以揭示修改后的代码或新代码应如何工作。另外,审阅者可以确认测试是否良好。
不幸的是,过于频繁的开发人员要么根本不编写单元测试,要么没有编写足够的测试,要么不维护它们。我了解——单元测试有时编写起来很棘手,或者维护起来很耗时。有时会有一个截止日期,感觉就像编写测试会让我们错过那个截止日期。但是没有编写足够的单元测试或没有编写好的单元测试是一个容易陷入的陷阱。
因此,请考虑以下有关如何编写干净、可维护的自动化测试的最佳实践建议,这些建议可以用最少的时间和精力为您提供单元测试的所有好处。
单元测试最佳实践
让我们看一些构建,运行和维护单元测试以达到最佳结果的最佳实践。
单元测试应该值得信赖
如果代码损坏并且只有代码损坏,则测试必须失败。否则,我们将无法相信测试结果在告诉我们什么。
单元测试应可维护且可读
当生产代码更改时,通常需要更新测试,也可能需要调试。因此,不仅对于编写它的人,而且对于其他开发人员,都必须易于阅读和理解该测试。为了清楚和易读,请始终组织和命名测试。
单元测试应验证单个用例
好的测试只能验证一件事,而只能验证一件事,这意味着通常情况下,它们只能验证一个用例。遵循此最佳实践的测试更简单,更易理解,这对于可维护性和调试很有好处。验证不止一件事的测试很容易变得复杂且维护耗时。不要让这种情况发生。
另一个最佳实践是使用最少数量的断言。有人建议每个测试只声明一个(可能有点太严格了)。这个想法是集中于仅验证所测试用例所需的内容。
单元测试应隔离
测试应该可以在任何机器上以任何顺序运行,而不会互相影响。如果可能,测试应不依赖于环境因素或全局/外部状态。具有这些依赖项的测试较难运行,并且通常不稳定,从而使其更难以调试和修复,最终花费的时间超过了所节省的时间(请参见上面的可信赖信息)。
几年前,马丁·福勒(Martin Fowler)撰写了有关“单独的”与“可社交的”代码的文章,以描述应用程序代码中的依赖关系用法,以及如何相应地设计测试。在他的文章中,“单独的”代码不依赖于其他单元(它更加独立),而“可联系的”代码确实与其他组件交互。如果应用程序代码是单独的,则测试很简单...但是对于正在测试的社交代码,您可以构建“单独”或“社交”测试。“社交测试”将依赖于真实的依赖关系以验证行为,而“单独测试”则将受测代码与依赖关系隔离开。您可以使用模拟来隔离被测代码,并为“可社交”代码构建“单独”测试。我们将在下面查看如何执行此操作。
图1:社交测试与孤立测试。资料来源:马丁·福勒(Martin Fowler),2014年,“UnitTest”
通常,使用模拟作为依赖项会使我们的测试人员生活更加轻松,因为我们可以为社交代码生成“单独的测试”。复杂代码的社交测试可能需要大量设置,并且可能违反隔离和可重复的原则。但是,由于模拟是在测试中创建和配置的,因此它是独立的,我们可以更好地控制依赖项的行为。另外,我们可以测试更多的代码路径。例如,我可以返回自定义值或从模拟中引发异常,以涵盖边界或错误情况。
单元测试应自动化
确保在自动化过程中运行测试。这可以是每天、每小时或在持续集成或交付过程中。团队中的每个人都需要访问并查看报告。作为一个团队,讨论您关心的指标:代码覆盖率、修改后的代码覆盖率、正在运行的测试数量、性能等。
通过查看这些数字可以学到很多东西,这些数字的巨大变化通常表明可以立即解决回归问题。
结合使用单元测试和集成测试
迈克尔·科恩(Michael Cohn)的书《成功实现敏捷:使用Scrum进行软件开发》使用测试金字塔模型解决了这一问题(请参见下图中的插图)。这是描述测试资源理想分配的常用模型。这个想法是,随着您进入金字塔,测试通常会更复杂、更脆弱、运行更慢、调试更慢。较低的级别更加隔离和集成、更快、更易于构建和调试。因此,自动化的单元测试应占您测试的大部分。
单元测试应验证所有细节、极端情况和边界条件等。应更谨慎地使用组件、集成、UI和功能测试,以验证API或应用程序的整体行为。手动测试应该在整个金字塔结构中所占的比例最小,但对于发布验收和探索性测试仍然有用。该模型为组织提供了高度的自动化和测试覆盖范围,因此他们可以扩大测试工作量,并将与构建、运行和维护测试相关的成本降至最低。
单元测试应在有组织的测试实践中执行
为了在各个级别上推动测试的成功,并使单元测试过程具有可扩展性和可持续性,您将需要一些其他实践。首先,这意味着在编写应用程序代码时编写单元测试。一些组织在应用程序代码之前编写测试(测试驱动或行为驱动的编程)。重要的是测试与应用程序代码紧密结合。测试和应用程序代码甚至应该在代码审查过程中一起审查。评论有助于您理解所编写的代码(因为他们可以看到预期的行为)并可以改善测试!
与代码一起编写测试不仅是针对新行为或计划中的更改,对于修复错误也至关重要。您修复的每个错误均应进行测试,以验证该错误是否已修复。这样可以确保该错误在将来保持不变。
对测试失败采取零容忍策略。如果您的团队忽略测试结果,那为什么还要进行测试呢?测试失败应该表明是真正的问题......因此,在浪费质量检查人员的时间之前或更早就解决这些问题,或更糟糕的是,它们会进入发布的产品。
解决故障所需的时间越长,这些故障最终将花费您的组织更多的时间和金钱。因此,在重构期间运行测试,请在提交代码之前立即运行测试,并且在测试通过之前也不要将任务视为“完成”。
最后,维护那些测试。正如我之前说过的,如果您在应用程序更改时没有使这些测试保持最新状态,则它们会失去价值。尤其是如果它们失败了,则失败的测试会浪费时间和金钱进行每次失败的调查。当代码更改时,根据需要重构测试。
如您所见,要使单元测试中的金钱和时间回报最大化,就需要在应用最佳实践方面进行一些投资。但最终,这些回报值得进行初始投资。
那代码覆盖率呢?
通常,代码覆盖率是对自动化测试运行期间执行了多少生产代码的度量。通过运行一组测试并查看代码覆盖率数据,您可以大致了解正在测试的应用程序数量。
代码覆盖范围很多,最常见的是行覆盖范围和分支覆盖范围。大多数工具专注于行覆盖率,它仅告诉您是否覆盖特定行。分支更加精细,因为它告诉您是否覆盖了代码的每个路径。
代码覆盖率是一项重要指标,但是请记住,增加覆盖率是达到目的的一种手段。这对于发现测试中的差距非常有用,但这并不是唯一要关注的事情。注意不要花费太多的精力来尝试达到100%的覆盖率——这甚至可能是不可能或不可行的,实际上,测试的质量是很重要的。话虽如此,为您的项目至少达到60%的覆盖率是一个不错的起点,而设定80%或更高的覆盖率是一个好的目标。显然,由您决定目标是什么。
如果您拥有自动化的工具,这不仅很有价值,它不仅可以测量代码覆盖率,还可以跟踪测试覆盖了多少修改后的代码,因为这可以使您了解是否编写了足够的测试以及生产代码的更改。
在此处查看来自Parasoft的报告和分析中心的示例代码覆盖率报告,如果您正在使用Parasoft Jtest进行单元测试,则可以浏览该示例:
要记住的另一件事是,在编写新测试时,请注意不要只关注行覆盖范围,因为单行代码可能会导致多个代码路径,因此请确保您的测试验证这些代码路径。线覆盖率是一个有用的快速指示器,但这并不是唯一要寻找的东西。
增加覆盖率的最明显方法就是简单地为更多的代码路径添加更多的测试,以及被测方法的更多用例。增加覆盖范围的有效方法是使用参数化测试。对于Junit4,有内置的Junit4参数化功能和诸如JunitParams之类的第三方库。Junit5具有内置的参数化功能。
最后,如果您尚未跟踪测试范围,强烈建议您开始。有很多工具可以提供帮助,例如Parasoft Jtest。首先测量您当前的覆盖范围数字,然后为应该覆盖的范围设定目标,首先解决重要的差距,然后再从那里开始工作。
总结
尽管单元测试是确保软件质量的可靠技术,但仍被认为是开发人员的负担,许多团队仍在为此而苦苦挣扎。为了充分利用测试和自动化测试工具,测试必须是可信赖的、可维护的、可读的、自包含的,并且必须用于验证单个用例。自动化是使单元测试可行和可扩展的关键。
此外,软件团队需要练习良好的测试技术,例如与应用程序代码一起编写和审查测试,维护测试以及确保立即跟踪和纠正失败的测试。采用这些单元测试最佳实践可以快速改善您的单元测试结果。