8个自动化测试的最佳实践以获得积极的测试体验

260 阅读25分钟

8个自动化测试的最佳实践以获得积极的测试体验

测试不一定是乏味的。有了这些自动化测试的最佳实践和技巧,软件工程师可以利用自动化测试来提高他们的生产力,使他们的工作更加愉快。

难怪许多开发人员认为测试是一种消耗时间和精力的必要之恶。测试可能是乏味的、无益的,而且完全过于复杂。

我的第一次测试经历是很糟糕的。我在一个有严格代码覆盖率要求的团队工作。工作流程是:实现一个功能,对其进行调试,然后写测试以确保代码的完全覆盖。这个团队没有集成测试,只有单元测试和大量的手动初始化模拟,大多数单元测试都是测试微不足道的手动映射,同时使用一个库来执行自动映射。每个测试都试图断言每一个可用的属性,所以每一个变化都会破坏几十个测试。

我不喜欢用测试工作,因为它们被认为是一个耗时的负担。然而,我并没有放弃。测试提供的信心和每次小改动后的自动化检查激起了我的兴趣。我开始阅读和练习,并了解到测试,如果做得对的话,可以是有帮助的令人愉快的。

在这篇文章中,我分享了八个自动化测试的最佳实践,我希望我一开始就知道。

为什么你需要一个自动化测试策略

自动化测试经常被关注于未来,但当你正确地实施它时,你会立即受益。使用帮助你更好地完成工作的工具可以节省时间,使你的工作更加愉快。

想象一下,你正在开发一个系统,从公司的ERP中检索采购订单,并将这些订单交给供应商。你在ERP中拥有以前订购的物品的价格,但目前的价格可能是不同的。你想控制是否以较低或较高的价格下订单。你已经存储了用户的偏好,你正在写代码来处理价格的波动。

你会如何检查代码是否按预期工作?你可能会这样做:

  1. 在开发人员的ERP实例中创建一个假的订单(假设你事先设置了它)。
  2. 运行你的应用程序。
  3. 选择该订单并开始下单过程。
  4. 从ERP的数据库中收集数据。
  5. 从供应商的API请求当前价格。
  6. 在代码中覆盖价格,创建特定的条件。

你停在断点处,可以一步一步地看一个场景会发生什么,但有很多可能的场景。

首选项ERP价格供应商价格我们应该下订单吗?
允许更高的价格允许更低的价格
------
错误虚假1010
(这里会有三个更多的偏好组合,但价格是相等的,所以结果是一样的。)
1011
错误109错误
虚假1011虚假
虚假109
1011true
true109true

如果出现错误,公司可能会损失金钱,损害声誉,或两者兼而有之。你需要检查多种情况,并多次重复检查循环。手动这样做会很乏味。但是,测试是来帮忙的!

测试让你在不调用不稳定的API的情况下创建任何上下文。它们消除了重复点击旧的和缓慢的接口的需要,这在传统的ERP系统中是非常普遍的。你所要做的就是为单元或子系统定义上下文,然后任何调试、故障排除或场景探索都会立即发生--你运行测试,你就可以回到你的代码。我的偏好是在我的IDE中设置一个绑定键,重复我之前的测试运行,当我做出改变时,立即给出自动反馈。

1.保持正确的态度

与手动调试和自我测试相比,自动化测试从一开始就更有成效,甚至在任何测试代码提交之前就已经开始了。在你通过手动测试,或者对于一个更复杂的模块,在测试过程中通过调试器来检查你的代码行为是否符合预期,你可以使用断言来定义你对任何输入参数组合的期望。

测试通过后,你几乎就可以提交了,但还没到时候。准备重构你的代码,因为第一个工作版本通常并不优雅。你会在没有测试的情况下进行重构吗?这是值得怀疑的,因为你必须再次完成所有的手工步骤,这可能会降低你的热情。

那么未来呢?在进行任何重构、优化或增加功能时,测试有助于确保一个模块在你改变它之后仍然表现得像预期的那样,从而灌输持久的信心,让开发人员感觉到更好的装备来处理即将到来的工作。

如果把测试看成是一种负担,或者是只让代码审查员或领导高兴的东西,那就适得其反了。测试是一种工具,我们作为开发者会从中受益。我们喜欢我们的代码工作,我们不喜欢把时间花在重复的动作上,或者花在修复代码以解决错误上。

最近,我在我的代码库中进行了重构,并要求我的IDE清理未使用的using 指令。令我惊讶的是,测试显示在我的电子邮件报告系统中出现了几个故障。然而,这是一个有效的失败--清理过程删除了我的Razor(HTML+C#)代码中的一些using 指令,用于电子邮件模板,模板引擎因此无法构建有效的HTML。我没有想到这样一个小操作会破坏电子邮件的报告。测试帮助我避免了在应用发布前花几个小时去捕捉所有的bug,当时我以为所有的东西都能正常工作。

当然,你必须知道如何使用工具,而不是切掉你那传说中的手指。似乎定义上下文很繁琐,可能比运行应用程序更难,测试需要太多的维护,以避免变得陈旧和无用。这些都是有效的观点,我们将解决它们。

2.选择正确的测试类型

开发人员往往渐渐不喜欢自动化测试,因为他们试图模拟一打的依赖关系,只是为了检查它们是否被代码所调用。或者,开发人员遇到一个高层次的测试,并试图重现每个应用程序的状态,以检查一个小模块的所有变化。这些模式是无益的和乏味的,但我们可以通过利用不同的测试类型来避免它们,因为它们本来就是如此。(毕竟,测试应该是实用的和令人愉快的!)

读者需要知道什么是单元测试以及如何编写单元测试,并熟悉集成测试--如果没有,值得在这里暂停一下,以提高速度。

有几十种测试类型,但这五种常见的类型是一个非常有效的组合。

五种常见的测试类型

  • 单元测试用于测试一个孤立的模块,直接调用其方法。依赖关系不被测试,因此,它们被模拟了。
  • 集成测试是用来测试子系统的。你仍然使用直接调用模块自己的方法,但在这里我们关心的是依赖关系,所以不要使用模拟的依赖关系,只使用真正的(生产)依赖模块。你仍然可以使用内存数据库或模拟的Web服务器,因为这些都是基础设施的模拟。
  • 功能测试是对整个应用程序的测试,也被称为端到端(E2E)测试。你不使用直接调用。相反,所有的交互都是通过API或用户界面进行的--这些是从终端用户的角度进行的测试。然而,基础设施仍然是模拟的。
  • 金丝雀测试类似于功能测试,但有生产基础设施和较小的动作集。它们被用来确保新部署的应用程序能够正常工作。
  • 负载测试类似于金丝雀测试,但有真实的暂存基础设施和更小的动作集,这些动作会重复多次。

并不总是需要从一开始就使用所有五个测试类型。在大多数情况下,你可以通过前三个测试走很长的路。

我们将简要检查每种类型的用例,以帮助你选择适合你的需求的类型。

单元测试

回顾一下有不同价格和处理偏好的例子。这是一个很好的单元测试的候选者,因为我们只关心模块内部发生的事情,而结果有重要的业务影响。

该模块有很多不同的输入参数组合,我们想为每个有效参数的组合得到一个有效的返回值。单元测试能很好地保证有效性,因为它们提供了对函数或方法的输入参数的直接访问,你不必写几十个测试方法来覆盖每一种组合。在许多语言中,你可以通过定义一个方法来避免重复的测试方法,这个方法接受你的代码和预期结果所需要的参数。然后,你可以使用你的测试工具为这个参数化的方法提供不同的值和预期。

集成测试

当你对一个模块与它的依赖,其他模块或基础设施的交互感兴趣时,集成测试是一个很好的适应情况。你仍然使用直接的方法调用,但没有对子模块的访问,所以试图测试所有子模块的所有输入方法的所有情况是不现实的。

通常情况下,我喜欢每个模块有一个成功场景和一个失败场景。

我喜欢用集成测试来检查一个依赖注入容器是否建立成功,一个处理或计算管道是否返回预期的结果,或者复杂的数据是否从数据库或第三方API正确读取和转换。

功能或E2E测试

这些测试让你对你的应用程序的运行最有信心,因为它们验证了你的应用程序至少可以在没有运行时错误的情况下启动。在不直接访问你的代码的情况下开始测试你的代码是比较麻烦的,但是一旦你理解并写出最初的几个测试,你会发现这并不难。

如果需要的话,通过启动一个带有命令行参数的进程来运行应用程序,然后像你的潜在客户那样使用该应用程序:通过调用API端点或按下按钮。这并不困难,即使是在UI测试的情况下。每个主要的平台都有一个工具来寻找UI中的视觉元素。

金丝雀测试

功能测试让你知道你的应用程序在测试环境中是否工作,但在生产环境中呢?假设你和几个第三方API一起工作,你想有一个它们状态的仪表板,或者想看看你的应用程序如何处理传入的请求。这些都是金丝雀测试的常见用例。

它们的操作方式是短暂地作用于工作系统,而不会对第三方系统造成副作用。例如,你可以注册一个新用户或检查产品的可用性而不下订单。

金丝雀测试的目的是确保所有的主要组件在生产环境中一起工作,而不是因为,例如,凭证问题而失败。

负载测试

负载测试揭示了当大量的人开始使用你的应用程序时,它是否能够继续工作。它们类似于金丝雀和功能测试,但不在本地或生产环境中进行。通常,使用一个特殊的暂存环境,它类似于生产环境。

值得注意的是,这些测试不使用真正的第三方服务,这些服务可能对其生产服务的外部负载测试不满意,并可能因此而收取额外费用。

3.保持测试类型的分离

当设计你的自动化测试计划时,每种类型的测试都应该分开,以便能够独立运行。虽然这需要额外的组织,但这是值得的,因为混合测试会产生问题。

这些测试有不同的:

  • 意图和基本概念(所以把它们分开,为下一个看代码的人,包括 "未来的你",树立了良好的先例)。
  • 执行时间(所以先运行单元测试,当测试失败时,可以更快地进行测试循环)。
  • 依赖关系(所以在一个测试类型中只加载需要的依赖关系会更有效)。
  • 所需的基础结构。
  • 编程语言(在某些情况下)。
  • 持续集成(CI)管道中的位置或外部位置。

值得注意的是,对于大多数语言和技术栈,你可以将所有单元测试分组,例如,以功能模块命名的子文件夹。这很方便,减少了创建新功能模块时的摩擦,更容易实现自动化构建,导致更少的混乱,也是简化测试的另一种方式。

4.自动运行你的测试

想象一下这样的情况:你写了一些测试,但在几周后拉出你的 repo 时,你发现这些测试不再通过。

这是一个令人不快的提醒,测试是代码,像任何其他代码一样,它们需要被维护。最好的时间是在你认为你已经完成了你的工作,并想看看一切是否仍按预期运行之前。你有所有需要的上下文,你可以比在不同子系统上工作的同事更容易地修复代码或改变失败的测试。但这个时刻只存在于你的脑海中,所以最常见的方式是在推送到开发分支或创建拉动请求后自动运行测试。

这样一来,你的主分支将始终处于有效状态,或者说,你至少可以清楚地看到它的状态。一个自动化的构建和测试管道--或CI管道--有助于:

  • 确保代码是可构建的。
  • 消除潜在的*"它在我的机器上可以工作 "*的问题。
  • 提供关于如何准备开发环境的可运行的指示。

配置该管道需要时间,但该管道可以在用户或客户接触到这些问题之前发现一系列问题,即使你是唯一的开发者。

一旦运行,CI也会在新问题有机会扩大范围之前发现它们。因此,我更喜欢在写完第一个测试后立即设置它。你可以把你的代码托管在GitHub上的一个私有仓库里,并设置GitHub动作。如果你的仓库是公开的,你甚至有比GitHub Actions更多的选择。例如,我的自动化测试计划在AppVeyor上运行,用于一个有数据库和三种测试的项目。

我更喜欢将生产项目的管道结构化,具体如下:

  1. 编译或转译
  2. 单元测试:它们是快速的,不需要依赖性
  3. 数据库或其他服务的设置和初始化
  4. 集成测试:它们有你的代码之外的依赖性,但它们比功能测试更快
  5. 功能测试:当其他步骤成功完成后,运行整个应用程序

没有金丝雀测试或负载测试。由于它们的特殊性和要求,它们应该是手动启动的。

5.只写必要的测试

为所有的代码编写单元测试是一种常见的策略,但有时这样做会浪费时间和精力,也不会给你带来任何信心。如果你熟悉 "测试金字塔 "的概念,你可能认为所有的代码都必须用单元测试来覆盖,只有一个子集被其他更高级别的测试覆盖。

我不认为有必要写一个单元测试来确保几个模拟的依赖关系按照所需的顺序被调用。这样做需要设置几个模拟并验证所有的调用,但这仍然不能让我确信模块在工作。通常,我只写一个集成测试,使用真实的依赖关系,只检查结果;这让我对被测模块的管道正常工作有一些信心。

一般来说,在实现功能和支持功能时,我写的测试能使我的生活更轻松。

对于大多数应用程序来说,以100%的代码覆盖率为目标会增加大量繁琐的工作,并消除测试工作和一般编程的乐趣。正如Martin Fowler的《测试覆盖率》所说的那样。

测试覆盖率是寻找代码库中未测试部分的一个有用工具。测试覆盖率作为一个数字说明你的测试有多好,用处不大。

因此我建议你在写完一些测试后安装并运行覆盖率分析器。带有高亮显示的代码行的报告将帮助你更好地理解其执行路径,并找到应该覆盖的未覆盖的地方。另外,看看你的getters、setters和facades,你就会明白为什么100%的覆盖率并不有趣。

6.玩乐高

我时常看到这样的问题:"我怎样才能测试私有方法?"你不会的。如果你问了这个问题,就说明已经出了问题。通常,这意味着你违反了单一责任原则,你的模块没有正确地做一些事情。

重构这个模块,把你认为重要的逻辑拉到一个单独的模块中。增加文件的数量是没有问题的,这将导致代码的结构像乐高积木一样:非常可读,可维护,可替换,可测试。

重构一个模块,使其类似于乐高积木。

正确的代码结构说起来容易做起来难。这里有两个建议。

函数式编程

值得学习一下函数式编程的原理和思想。大多数主流语言,如C、C++、C#、Java、Assembly、JavaScript和Python,迫使你为机器编写程序。函数式编程更适合于人脑。

这一点起初可能有悖常理,但请考虑一下。如果你把所有的代码都放在一个方法中,使用共享内存块来存储临时值,并使用相当数量的跳转指令,计算机就会很好。此外,处于优化阶段的编译器有时会这样做。然而,人类的大脑并不容易处理这种方法。

函数式编程迫使你以一种表达式的方式编写没有副作用的纯函数,并使用强类型。这样一来,对一个函数的推理就容易多了,因为它产生的唯一东西就是它的返回值。Programming Throwdown播客中的Functional Programming With Adam Gordon Bell一集将帮助你获得基本的理解,你可以继续阅读Corecursive中的God's Programming Language With Philip WadlerCategory Theory With Bartosz Milewski。后两集大大丰富了我对编程的认识。

测试驱动的开发

我建议掌握TDD。学习的最好方法是实践。字符串计算器卡塔是练习代码卡塔的一个好方法。掌握卡塔需要时间,但最终会让你完全吸收TDD的理念,这将有助于你创建结构良好的代码,让人乐于使用,同时也是可测试的。

有一点需要注意的是。有时你会看到TDD纯粹主义者声称TDD是唯一正确的编程方式。在我看来,它只是你的工具箱中另一个有用的工具,仅此而已。

有时,你需要看到如何调整模块和进程之间的关系,而不知道该使用什么数据和签名。在这种情况下,写代码直到它被编译,然后写测试来排除故障和调试功能。

在其他情况下,你知道你想要的输入和输出,但由于复杂的逻辑,不知道如何正确地编写实现。对于这些情况,开始遵循TDD程序,一步一步地构建你的代码,而不是花时间考虑完美的实现。

7.保持测试的简单性和专注性

在一个整洁的代码环境中工作,没有不必要的干扰,是一种享受。这就是为什么将SOLIDKISSDRY原则应用于测试的重要原因--在需要时利用重构。

有时我听到这样的评论:"我讨厌在一个经过大量测试的代码库中工作,因为每一个变化都需要我修复几十个测试"。这是一个高维护率的问题,是由于测试不集中和试图测试太多造成的。做好一件事 "的原则也适用于测试。"做好一件事";每个测试应该相对简短,只测试一个概念。"测试好一件事 "并不意味着你应该在每个测试中仅限于一个断言。如果你在测试非微不足道的重要数据映射,你可以使用几十个。

这种关注并不限于一个特定的测试或测试类型。想象一下,处理复杂的逻辑,你用单元测试来测试,比如把数据从ERP系统映射到你的结构中,你有一个集成测试,正在访问模拟的ERP APIs,并返回结果。在这种情况下,重要的是记住你的单元测试已经涵盖的内容,这样你就不会在集成测试中再次测试映射。通常情况下,确保结果有正确的识别字段就足够了。

有了像乐高积木一样的代码结构和集中测试,对业务逻辑的改变不应该是痛苦的。如果改变是激进的,你只需放弃文件和它的相关测试,然后用新的测试做一个新的实现。如果是微小的变化,你通常会改变一到三个测试来满足新的要求,并对逻辑进行修改。改变测试是没有问题的;你可以把这种做法看成是重复记账

其他实现简化的方法包括:

  • 制定测试文件结构、测试内容结构(通常是排列-行为-插入结构)和测试命名的惯例;然后,最重要的是,一致地遵守这些规则。
  • 将大的代码块提取到 "准备请求 "这样的方法中,并为重复的动作制作辅助函数。
  • 应用构建者模式来配置测试数据。
  • 使用(在集成测试中)你在主应用程序中使用的相同的DI容器,所以每一个实例化都会像TestServices.Get() ,无需手动创建依赖关系。这样一来,阅读、维护和编写新的测试就会很容易,因为你已经有了有用的帮助工具。

如果你觉得一个测试变得太复杂,只需停下来想想。无论是模块还是你的测试都需要重构。

8.使用工具,使你的生活更轻松

在测试时,你将面临许多繁琐的任务。例如,设置测试环境或数据对象,为依赖关系配置存根和模拟,等等。幸运的是,每一个成熟的技术栈都包含一些工具,使这些任务不那么繁琐。

我建议,如果你还没有写出你的第一百个测试,那么就投入一些时间来识别重复的任务,并了解你的技术栈的测试相关工具。

为了获得灵感,这里有一些你可以使用的工具:

  • 测试运行器:寻找简洁的语法和易于使用的工具。根据我的经验,对于.NET,我推荐xUnit(尽管NUnit也是一个可靠的选择)。对于JavaScript或TypeScript,我选择Jest。试着找到最适合你的任务和心态的匹配,因为工具和挑战是不断变化的。
  • 嘲讽库:对于代码的依赖性,可能有低级别的模拟,如接口,但也有更高级别的模拟,如网络API或数据库。对于JavaScript和TypeScript,Jest中包含的低级别的模拟是可以的。对于.NET来说。我使用Moq,尽管NSubstitute也很棒。至于网络API模拟,我喜欢使用WireMock.NET。它可以代替API用于故障排除和调试响应处理。它在自动化测试中也非常可靠和快速。数据库可以使用它们的内存对应物进行模拟。.NET中的EfCore提供了这样一个选项。
  • 数据生成库:这些实用程序用随机数据填充你的数据对象。例如,当你只关心大数据传输对象中的几个字段时,它们很有用(如果有的话;也许你只想测试映射的正确性)。你可以把它们用于测试,也可以作为随机数据显示在表单上或填充你的数据库。为了测试目的,我在.NET中使用AutoFixture。
  • UI自动化库:这些是自动测试的自动化用户。他们可以运行你的应用程序,填写表格,点击按钮,阅读标签,等等。为了浏览你的应用程序的所有元素,你不需要处理通过坐标或图像识别的点击;主要的平台都有工具,可以通过类型、标识符或数据找到需要的元素,所以你不需要在每次重新设计时改变你的测试。它们是健壮的,所以一旦你让它们为你和CI工作(有时你会发现事情只在你的机器上工作),它们将继续工作。我喜欢在.NET中使用FlaUI,在JavaScript和TypeScript中使用Cypress。
  • 断言库:大多数测试运行程序包括断言工具,但在有些情况下,独立的工具可以帮助你使用更干净、更可读的语法编写复杂的断言,比如用于.NET的Fluent断言。我特别喜欢断言集合是相等的功能,无论项目的顺序或其在内存中的地址如何。

愿流动与你同在

幸福与所谓的 "流程 "经验紧密相连,在书中有详细描述 流动:最佳体验的心理学.为了实现这种流动体验,你必须参与到一个有明确目标的活动中,并且能够看到你的进展。任务应该产生即时的反馈,为此,自动化测试是理想的选择。你还需要在挑战和技能之间取得平衡,这取决于每个人。测试,特别是当用TDD来处理时,可以帮助指导你并灌输信心。他们帮助你设定具体的目标,每一个通过的测试都是你进步的指标。

正确的测试方法可以使你更快乐,更有效率,测试可以减少倦怠的机会。关键是要把测试看作是一种工具(或工具集),可以帮助你的日常开发工作,而不是把它看作是为你的代码提供未来保障的一个累赘。

测试是编程的一个必要部分,它使软件工程师能够改进他们的工作方式,提供最好的结果,并最佳地利用他们的时间。也许更重要的是,测试可以帮助开发人员更加享受他们的工作,从而提高他们的士气和动力。

了解基础知识

自动化测试是如何工作的?

自动化测试执行你的生产代码,并确保其行为符合预期。

自动化测试是用来做什么的?

首先,自动化测试是用来帮助你编写和排除你的代码的故障。其次,它确保你的代码在重构、优化或其他变化后仍然可以工作。

自动化测试困难吗?

如果做得好,自动化测试并不难。学习如何正确地做,需要投入一些时间来掌握新的技能,并知道何时使用每个技能以获得最大的效果。

为什么我们需要自动化测试?

我们需要自动化测试来优化我们编写一个功能的时间。它使我们能够在编写新代码时进行故障排除和测试,并减少以后支持该功能所需的时间。

自动化测试值得吗?

自动化测试绝对是值得的。当你掌握了它,即使在处理短命的原型项目时,投入测试的努力也是值得的。