作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
列夫·亚斯特列博夫的头像

By 列弗Yastrebov

Lev是一个很有成就的c#和 .利用测试驱动开发的。网。开发人员, 静态分析, 并对技术有深入的了解,以稳健的方式解决任务, 干净代码. 在领先的智能海洋技术公司瓦锡兰, 他创造了一种3D海底测绘算法,可以解决数千份海图中相互矛盾的数据.

专业知识

以前的角色

高级软件工程师

工作经验

16

以前在

瓦锡兰
分享

难怪许多开发人员认为测试是消耗时间和精力的必要之恶:测试可能是乏味的, 非生产性的, 太复杂了.

我的第一次考试经历很糟糕. 我所在的团队有严格的代码覆盖要求. 工作流程是:实现一个特性,调试它,然后编写测试以确保完整的代码覆盖. 团队没有集成测试, 只进行带有大量手动初始化模拟的单元测试, 而且,大多数单元测试在使用库执行自动映射的同时测试琐碎的手动映射. 每个测试都试图断言每个可用属性,因此每次更改都会破坏数十个测试.

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

在本文中,我将分享8个 自动化测试 我希望从一开始就知道的最佳实践.

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

自动化测试通常关注未来, 但是当你正确地实现它, 你马上就会受益. 使用可以帮助你更好地完成工作的工具可以节省时间,让你的工作更愉快.

假设您正在开发一个系统,该系统从公司的ERP中检索采购订单,并将这些订单交给供应商. 您在ERP中有以前订购的物品的价格,但当前的价格可能不同. 您想要控制是以较低还是较高的价格下订单. 您已经存储了用户偏好,并且正在编写处理价格波动的代码.

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

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

您在断点处停下来,可以一步一步地查看一个场景会发生什么, 但有很多可能的情况:

首选项ERP的价格供应商的价格我们应该下订单吗?
允许更高的价格允许降低价格
1010真正的
(这里还有另外三种偏好组合, 但是价格是相等的, 所以结果是一样的.)
真正的1011真正的
真正的109
真正的1011
真正的109真正的
真正的真正的1011真正的
真正的真正的109真正的

一旦出现漏洞,公司可能会损失金钱,损害声誉,或者两者兼而有之. 需要检查多个场景,重复检查多次. 手动这样做会很乏味. 但测试是有帮助的!

测试允许您创建任何上下文,而无需调用不稳定的api. 它们消除了在旧的ERP系统中非常常见的旧而缓慢的界面上重复点击的需要. 您所要做的就是为单元或子系统定义上下文,然后进行调试, 故障排除, 或者,场景探索会立即发生—您运行测试并返回到您的代码. 我倾向于在IDE中设置一个键绑定,以重复之前的测试运行, 立即给, 当我做出改变时自动反馈.

1. 保持正确的态度

与手工调试和自测相比, 自动化测试从一开始就更有生产力, 甚至在提交任何测试代码之前. 在检查代码是否按预期行为之后 手动测试 或者, 对于更复杂的模块, 通过在测试期间使用调试器逐步调试它,您可以使用断言来定义您期望的任何输入参数组合.

测试通过后,您就差不多可以提交了,但还没有完全准备好. 准备重构你的代码,因为第一个可用的版本通常并不优雅. 你会在没有测试的情况下执行重构吗? 这是值得怀疑的,因为您必须重新完成所有手动步骤, 这会降低你的热情吗.

未来呢?? 在执行任何重构时, 优化, 或者特性添加, 测试有助于确保模块在更改后仍按预期运行, 从而灌输持久的信心,让开发人员能够更好地处理即将到来的工作.

认为测试是一种负担,或者只是让代码审查者或领导高兴的事情,这是适得其反的. 测试是我们开发人员可以从中受益的工具. 我们喜欢我们的代码能够正常工作,我们不喜欢花时间在重复的操作或修复代码以解决错误上.

最近,我致力于重构我的代码库,并要求我的IDE清理未使用的代码 使用 指令. 令我惊讶的是,测试显示我的电子邮件报告系统出现了几处故障. 然而,这是一个有效的失败——清理过程删除了一些 使用 指令在我的Razor (HTML + c#)代码中用于电子邮件模板, 因此模板引擎无法构建有效的HTML. 我没想到这样一个小操作会破坏电子邮件报告. 测试帮助我避免了在应用发布前花费数小时去寻找漏洞, 当我以为一切都会顺利的时候.

当然,你必须知道如何使用工具,而不是割伤你的手指. 定义上下文似乎很乏味,可能比运行应用程序更难, 测试需要太多的维护以避免过时和无用. 这些都是有效的观点,我们将予以解决.

2. 选择正确的测试类型

开发人员通常会逐渐不喜欢自动化测试,因为他们试图模拟一堆依赖项,只是为了检查它们是否被代码调用. 另外, 开发人员遇到高级测试,并尝试重现每个应用程序状态,以检查小模块中的所有变化. 这些模式是低效且乏味的, 但是我们可以通过利用不同的测试类型来避免它们. (毕竟,考试应该是实用而有趣的!)

读者需要知道是什么 单元测试是什么以及如何编写单元测试,并熟悉 集成测试-如果没有,值得在这里停下来了解一下.

有几十种测试类型, 但这五种常见类型的组合非常有效:

一组描述单元测试的基本插图, 集成测试, 功能测试, 金丝雀测试, 负载测试.
五种常见的测试类型

  • 单元测试 是用来测试一个孤立的模块,直接调用它的方法. 依赖性没有被测试,因此,它们被嘲笑了.
  • 集成测试 用于测试子系统. 您仍然可以直接调用模块自己的方法, 但这里我们关心的是依赖关系, 所以不要使用模拟的依赖——只使用真正的(生产)依赖模块. 您仍然可以使用内存中的数据库或模拟的web服务器,因为这些都是基础设施的模拟.
  • 功能测试 测试是针对整个应用程序的吗. 你不用直拨电话. 而不是, 所有的交互都是通过API或用户界面进行的——这些是从最终用户的角度进行的测试. 然而,基础设施仍然受到嘲笑.
  • 金丝雀测试 是否类似于功能测试,但使用生产基础设施和更小的操作集. 它们用于确保新部署的应用程序能够正常工作.
  • 负载测试 是否类似于金丝雀测试,但使用的是真正的分级基础设施和更小的操作集, 哪些重复了很多次.

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

我们将简要地检查每种类型的用例,以帮助您根据需要选择正确的用例.

单元测试

回想一下具有不同价格和处理偏好的示例. 它是单元测试的一个很好的候选,因为我们只关心模块内部发生了什么, 其结果具有重要的商业影响.

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

集成测试

当您对模块如何与其依赖项交互感兴趣时,集成测试非常适合, 其他模块, 或者基础设施. 您仍然使用直接方法调用,但无法访问子模块, 因此,试图测试所有子模块的所有输入法的所有场景是不切实际的.

通常,我更喜欢每个模块有一个成功场景和一个失败场景.

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

功能测试

这些测试给你最大的信心,你的应用程序工作,因为他们验证你的应用程序至少可以启动没有运行时错误. 在不直接访问代码类的情况下开始测试代码需要做更多的工作, 但是一旦您理解并编写了前几个测试, 你会发现这并不难.

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

金丝雀测试

功能测试让你知道你的应用程序是否在测试环境中工作,但在生产环境中呢? 假设您正在使用几个第三方api,并且希望有一个显示它们状态的指示板,或者希望查看应用程序如何处理传入请求. 这些是金丝雀测试的常见用例.

它们通过短暂地作用于工作系统而不会对第三方系统产生副作用来运行. 例如,您可以在不下订单的情况下注册新用户或检查产品可用性.

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

负载测试

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

需要注意的是,这些测试不使用真正的第三方服务, 哪些公司可能对其生产服务的外部负载测试不满意,并可能因此收取额外费用.

3. 保持测试类型分离

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

这些测试有不同的:

  • 意图和基本概念(因此将它们分开)为下一个查看代码的人树立了良好的先例, 包括“未来的你”).
  • 执行时间(因此,当测试失败时,首先运行单元测试可以加快测试周期).
  • 依赖项(因此只加载测试类型中所需的依赖项会更有效).
  • 所需的基础设施.
  • 编程语言(在某些情况下).
  • 在持续集成(CI)管道内或管道外的位置.

需要注意的是,对于大多数语言和技术栈, 你可以分组, 例如, 所有单元测试以及以功能模块命名的子文件夹. 这很方便, 减少创建新功能模块时的摩擦, 自动构建是否更容易, 减少杂乱, 这是简化测试的另一种方法.

4. 自动运行测试

假设您已经编写了一些测试, 但几周后,你收回了回购, 您注意到这些测试不再通过.

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

这种方式, 您的主分支将始终处于有效状态, 或者你会, 至少, 对它的状态有明确的指示吗. 自动化的构建和测试管道(或者CI管道)有助于:

  • 确保代码是可构建的.
  • 消除潜在的 “它在我的机器上运行” 问题.
  • 提供关于如何准备开发环境的可运行说明.

配置这个管道需要时间, 但是,在信息到达用户或客户之前,这条管道可能会暴露出一系列问题, 即使你是唯一的开发者.

一旦运行,CI还会在新问题有机会扩大范围之前揭示它们. 因此,我更喜欢在编写第一个测试后立即设置它. 你可以在GitHub的私有存储库中托管你的代码,并设置GitHub Actions. 如果你的repo是公开的,你有比GitHub Actions更多的选择. 例如, 我的自动化测试计划 在AppVeyor上运行,用于具有数据库和三种类型测试的项目.

我倾向于按照以下方式构建我的生产项目管道:

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

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

5. 只写必要的测试

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

我认为没有必要编写一个单元测试,以确保几个模拟依赖项按所需的顺序调用. 这样做需要设置几个模拟并验证所有调用, 但这仍然不能让我相信这个模块正在工作. 通常, I only write an integration test that uses real dependencies 和 checks only the result; that gives me some confidence that the pipeline in the tested module is working properly.

在一般情况下, 我编写测试,使我在实现功能和支持功能时更轻松.

对于大多数应用程序, 以100%的代码覆盖率为目标增加了大量乏味的工作,并且消除了处理测试和编程的乐趣. 正如马丁·福勒的 测试覆盖率 所说:

测试覆盖率是查找代码库中未测试部分的有用工具. 测试覆盖率作为测试好坏的数字陈述没有多大用处.

因此,我建议您在编写一些测试后安装并运行覆盖率分析器. 带有突出显示的代码行的报告将帮助您更好地理解其执行路径,并找到应该覆盖的未覆盖的地方. 此外,查看getter、setter和facade,您将看到为什么100%的覆盖并不有趣.

6. 乐高玩

不时地,我看到这样的问题:“我如何测试私有方法?“你不知道。. 如果你问了这个问题,那一定是出问题了. 通常,这意味着你违反了 单一责任原则,并且您的模块不能正确地执行某些操作.

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

左边是一堆矩形. 最上面的标记为OrderProcessor,下面的一些标记为Access Order Data, 价格检查, 及下订单. 一个箭头从左边指向右边, 在哪里OrderProcessor是一个侧面乐高积木, 砖块在不同的阶段被连接和分离, 包括OrderDataProvider, PriceChecker, 和OrderPlacer.
将模块重构成类似乐高积木的样子.

正确地构建代码说起来容易做起来难. 这里有两个建议:

函数式编程

函数式编程的原则和思想是值得学习的. 大多数主流语言, 像C, C++, C#, Java, 组装, JavaScript, 和Python, 强迫你为机器编写程序. 函数式编程更适合人脑.

乍一看,这似乎有悖常理, 但是考虑到这一点:如果您将所有代码放在一个方法中,计算机将会很好, 使用共享内存块来存储临时值, 并且使用相当数量的跳转指令. 此外,在优化阶段的编译器有时也会这样做. 然而,人类的大脑并不容易处理这种方法.

函数式编程迫使您编写没有副作用的纯函数, 使用强类型, 以富有表现力的方式. 这样推理一个函数就容易多了,因为它产生的唯一东西就是它的返回值. 节目简介播客 函数式编程与亚当戈登贝尔 将帮助你获得一个基本的理解,你可以继续进行共递归的情节 Philip Wadler的《欧博体育app下载》Bartosz milwski的范畴理论. 后两者极大地丰富了我对编程的认识.

测试驱动开发

我建议你掌握 TDD. 最好的学习方法是练习. 串计算器形 是练习的好方法吗 代码型. 掌握这种类型需要时间,但最终会让你完全吸收TDD的思想, 哪一种方法可以帮助您创建结构良好、易于使用且可测试的代码.

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

有时, 您需要了解如何调整模块和进程之间的关系,而不知道使用什么数据和签名. 在这种情况下, 编写代码直到编译完成, 然后编写测试以排除故障并调试功能.

在其他情况下, 你知道想要的输入和输出, 但由于逻辑复杂,不知道如何正确地编写实现. 对于这些情况, 遵循TDD过程并一步一步地构建代码比花时间考虑完美的实现要容易得多.

7. 保持测试的简单和重点

在一个整洁的代码环境中工作是一种乐趣,没有不必要的干扰. 这就是为什么申请很重要 固体, , 测试原则——在需要的时候利用重构.

有时我听到这样的评论, “我讨厌在经过大量测试的代码库中工作,因为每次更改都需要我修复数十个测试.“这是一个高维护的问题,是由测试不集中和试图测试太多造成的. The principle of “Do one thing well” applies to tests too: “Test one thing well”; each test should be relatively short 和 test only one concept. “很好地测试一件事”并不意味着您应该将每个测试限制为一个断言:如果您正在测试重要的数据映射,您可以使用数十个断言.

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

用像乐高积木一样的代码和集中的测试, 更改业务逻辑不应该是痛苦的. 如果变化是激进的, 您只需删除该文件及其相关测试, 用新的测试做一个新的实现. 如果有微小的变化, 您通常更改一到三个测试以满足新的需求并更改逻辑. It’s fine to change tests; you can think about 这种做法称为复式记账法.

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

  • 提出了测试文件结构的约定, 测试内容结构(通常是安排-行为-断言结构), test naming; then, 最重要的是, 始终遵循这些规则.
  • 将大的代码块提取到“准备请求”等方法中,并为重复的操作创建辅助函数.
  • 应用 构建者模式 测试数据配置.
  • 使用(在集成测试中)你在主应用中使用的相同的DI容器,这样每个实例化都将是微不足道的 TestServices.Get () 无需手动创建依赖项. 这样就容易读了, 维护, 并编写新的测试,因为您已经有了有用的帮助程序.

如果你觉得考试变得太复杂了,只要停下来想一想. 模块或测试都需要重构.

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

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

如果您还没有编写前100个测试,我建议您编写, 然后投入一些时间来识别重复性任务,并了解与测试相关的技术栈工具.

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

  • 测试运行. 寻找简洁的语法和易用性. 从我的经验来看,为 .我推荐xUnit(尽管NUnit也是一个不错的选择). 对于JavaScript或TypeScript,我使用Jest. 试着为你的任务和心态找到最匹配的,因为工具和挑战是不断变化的.
  • 嘲笑库. 可能存在代码依赖关系的低级模拟, 像接口, 但也有针对web api或数据库的高级模拟. 对于JavaScript和TypeScript, Jest中包含的低级mock是可以的. 为 .网。. 我使用 Moq,不过NSubstitute也很棒. 至于web API模拟,我喜欢使用WireMock.网。. 它可以代替API用于故障排除和调试响应处理. 它在自动化测试中也非常可靠和快速. 可以使用内存中的数据库来模拟数据库. EfCore在 .网。提供了这样一个选项.
  • 数据生成库. 这些实用程序用随机数据填充数据对象. 它们很有用, 例如, you only care about a couple of fields from a big data transfer object (if that; maybe you only want to test mapping correctness). 可以将它们用于测试,也可以作为随机数据显示在表单上或填充数据库. 为了测试的目的,我在 .网。.
  • UI自动化库. 这些是自动化测试的自动化用户:他们可以运行你的应用程序, 填写表格, 点击按钮, 阅读标签, 等等......。. 浏览应用程序的所有元素, you don’t need to deal with clicking by coordinates or image recognition; major platforms have the tooling to find needed elements by type, 标识符, 或者数据,这样您就不需要在每次重新设计时更改测试. 它们很健壮, 因此,一旦你让它们为你和CI工作(有时你会发现它们只在工作上) 你的机器),他们会继续工作. 我喜欢使用FlaUI .. 网。和Cypress用于JavaScript和TypeScript.
  • 断言库. 大多数测试运行程序都包含断言工具, 但是在某些情况下,独立的工具可以帮助您使用更清晰、更易读的语法编写复杂的断言, 比如Fluent断言 .网。. 我特别喜欢断言集合相等的函数,无论项的顺序或其在内存中的地址如何.

通过测试提高幸福感

测试,特别是在使用TDD时,可以帮助指导您并灌输信心. 他们帮助你设定具体的目标,每一次通过的测试都是你进步的一个指标.

正确的测试方法可以让你更快乐,更有效率, 而且测试减少了倦怠的机会. 关键是将测试视为一种工具(或工具集),可以帮助您进行日常开发, 而不是作为一个繁琐的步骤来验证您的代码.

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

了解基本知识

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

    自动化测试执行您的产品代码,并确保它按照预期的方式运行.

  • 自动化测试的用途是什么?

    首先,自动化测试用于帮助您编写代码并排除故障. 其次,它可以确保您的代码在重构、优化或其他更改后仍然可以正常工作.

  • 自动化测试困难吗??

    如果操作得当,自动化测试并不困难. 学习如何正确地做这件事需要投入一些时间来掌握新技能,并知道何时使用每一种技能以获得最大的效果.

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

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

  • 自动化测试值得吗?

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

聘请Toptal这方面的专家.
现在雇佣
列夫·亚斯特列博夫的头像
列弗Yastrebov

位于 土耳其安塔利亚

成员自 2020年4月29日

作者简介

Lev是一个很有成就的c#和 .利用测试驱动开发的。网。开发人员, 静态分析, 并对技术有深入的了解,以稳健的方式解决任务, 干净代码. 在领先的智能海洋技术公司瓦锡兰, 他创造了一种3D海底测绘算法,可以解决数千份海图中相互矛盾的数据.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

以前的角色

高级软件工程师

工作经验

16

以前在

瓦锡兰

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.