测试简介

已发表: 2021-06-16

欢迎来到我们关于与测试相关的所有内容的新博客系列。 希望这些博客文章能让您大致了解我们在 Mediatoolkit 是如何编写它们的,以及为什么。 在深入研究示例之前,让我们从一些基本定义、想法和编写测试的好处开始。

那里有什么样的测试?

并非所有测试都是平等的。 有不同种类的测试用于不同的目的。 虽然这篇文章主要关注单元测试,但我们应该意识到它们之间的差异并了解它们的好处。

1. 手动测试

手动测试涉及由测试人员手动执行的测试。 这些通常由质量保证部门或开发人员自己完成。 QA 应该被视为最后一道防线,而不是主要的应用程序测试人员(QA 将手动测试作为 UI 测试的主要方式是这种情况的有效例外)。

当开发人员想要在本地运行一些东西查看系统如何运行而不将任何代码推送到暂存阶段时,开发人员通常会编写手动测试,或者上帝保佑你,生产。 然而,这些测试的目标并不是代码的健壮性。 这些测试的唯一作用是捕捉错误并确保您的产品质量

2. 集成测试

系统由多个组件组成,或者至少它们应该是。 集成测试检查这些组件的公共 API 。 不仅仅是 REST API,还有任何公开的 API,比如你的 Kafka 主题通信。

如果一个组件“说”它以某种格式输出关于某个主题的消息,那就是它的公共 API 或合约。 集成测试正在检查多个单独开发的组件是否可以通信,以及它们的合同是否作为一个组匹配。

如果您只单独测试这些组件,它们的个别行为可能会正常工作,但是当您尝试将它们连接到更大的组中时,您会发现它们的合同不同

3.端到端(E2E)测试

端到端测试是保证最终产品的用户体验流程。 当今系统的复杂性很难用测试来覆盖。 许多系统相互依赖,E2E 测试确保产品达到预期效果。 QA 通过检查最终用户流程并检查所有系统是否按预期运行来验证产品的正确性。

4.单元测试

单元测试是任何可靠软件的支柱。 它们为其他测试奠定了基础。 他们测试单个单元。

开发人员可能会将单元的定义误认为是单一的方法。 单元测试应该测试可能由多个不同类组成的组件的行为。 其他组件可访问的公共方法需要测试,不要测试受保护的方法或类。 它们是实现细节,而不是您的公共 API 的一部分

为什么我们还要测试?

如果我们编写好的测试并经常编写它们,我们可以在产品出现之前确保它的质量。

随着我们系统的老化,测试的好处变得越来越明显。 我们的环境变得可靠,它们节省了开发时间,并在不可避免地出现问题时减少了很多麻烦。 当您的同事能够查看您的测试并了解您的代码应该做什么和不应该做什么而不需要手动运行时,他们会很感激他们。

健壮的代码

在我们开始谈论“健壮代码”之前,我们应该如何定义它? 是什么让代码健壮? 这是否意味着我们应该进行防御性编程并考虑其他开发人员可能会如何滥用我们的代码?

健壮的代码总是简单而干净的。 简单是健壮的,复杂是脆弱的。 我们应该处理无效的输入,但这并不意味着我们需要防御性地编程并且不信任我们的团队。

高尔定律:一个有效的复杂系统总是被发现是从一个有效的简单系统演变而来的。

相反的命题似乎也是正确的:从零开始设计的复杂系统永远不会工作,也无法使其工作。 你必须重新开始,从一个工作简单的系统开始。

安全地重构

当您的代码被测试覆盖时,您不再害怕更改现有代码。 每次更改后,您都可以运行测试并确保没有破坏任何东西。 当您进行测试时,您不必进行防御性编程。

没有测试的重构正在走下一个滑坡,最终会在不眠之夜和周日工作中结束。 这个话题太宽泛,无法在此涵盖,未来值得单独发表一篇博文。

我们怎么不写测试?

知道如何不编写单元测试与如何编写单元测试同样重要。

  • 编写打印方法调用结果的测试不是测试,因为我们不验证所需的结果
  • 如果您的测试从 Documents 文件夹中的文件读取数据,则它不是真正的单元测试,因为测试不应依赖于环境。
  • 任何开发人员都应该能够检查您的代码并成功运行测试,而无需执行任何其他操作。
  • 每个单元测试都应该独立于其他测试。 这意味着测试的执行顺序也不重要。
  • 如果我们不更改任何代码,多次运行测试应该总是以相同的结果结束
  • 单元测试应该测试行为,而不是单独的方法调用。 并非每个类和方法都需要进行测试。

行为是产生系统用户需要的真正价值的东西。 您的用户是否需要知道productFactory.create()在调用两次时是否创建了相同的对象,或者是否使用某些参数调用了您的存储库? 可能不会,但仍然有许多开发人员编写了这些类型的测试。

如果您的测试看起来像那样,它们与您的实现紧密耦合。 每次您想要更改实现的细节时,您都需要更新您的测试,即使行为是相同的。 你的测试应该只在行为改变时改变,而不是实现细节。 换句话说,测试的代码做了什么,而不是它是怎么做的。

我们如何编写测试?

我们的测试必须遵循最佳代码实践,它们必须独立于环境并且需要快速执行

保持测试执行时间尽可能短很重要。 每个测试不应超过几毫秒。 当测试执行时间过长时,人们往往会跳过它们,只依赖他们的 CI 服务器,例如 Jenkins,当它无法构建他们的部署可执行文件时会大惊小怪。

每个测试由3 个“A”部分(AAA 模式)组成:

  1. 安排
  2. 行为
  3. 断言

1. 安排

在我们测试的安排部分,我们确保我们的系统在调用我们想要测试的行为之前处于特定状态。 “系统”可能是一个对象,我们需要以特定方式设置它以产生行为、创建临时文件或类似性质的东西。

本节中的代码通常比其他两个加起来要大

Object Mother 应该证明对保持这部分的小而特别有用的一种设计模式是Object Mother 。 这种设计模式与Factory非常相似,但它有更具体的方法可以为您构建预配置的对象。 虽然标准Factory可能有诸如createCar(carDescription)类的方法,但ObjectMother将具有诸如createRedFerrari()createBlackTesla()createBrokenYugo()类的方法。

2.行动

这部分测试必须有一行。 此行执行被测行为。 如果您发现自己为这一部分写了不止一行,那么您可能没有正确封装您的行为。 不应期望您的客户以特定顺序调用对象的多个方法,那么为什么要进行测试呢?

这一行是我们要测试的方法调用。 如果此方法返回结果,您应该将该值存储在一个变量中,以检查它是否是 Assert 步骤中的预期值。

3. 断言

当我们在 Arrange 部分准备好系统并在 Act 部分执行我们的被测动作后,我们需要验证动作的结果。 我们通常在这里检查方法的结果,但有时,我们的方法不返回值,但它们仍然会产生副作用。 如果我们的代码需要改变一个对象的状态,创建一个文件,或者从List中删除一些东西,我们应该检查它是否确实做到了。

存根与模拟

大多数开发人员交替使用术语模拟存根,但存在差异。

存根不能通过测试,模拟可以。

存根是基于状态的,它们返回硬编码值。 “我得到了什么结果?”

模拟是基于行为的,您可以使用它们来验证您的行为如何通过它。 “我是怎么得到结果的?”

如果您的单元测试充满了模拟,那么您的测试最终会变得非常脆弱,这意味着每次您更改一个实现细节时,您都需要更新所有模拟调用。

只测试一件事。

我们需要能够隔离一种行为并证明它有效。 如果该行为在不同的输入下应该以不同的方式工作,我们需要为这些行为中的每一个编写一个新的测试。 如果我们有一个同时测试多个事物的大型测试,就很难知道为什么我们的测试失败了。 此外,删除我们不再需要的功能并查看我们在添加新代码时破坏了哪些功能变得更加困难。

在测试的最后部分有多个断言是完全可以的,只要这不需要您多次调用被测行为即可。 如果是这种情况,将很难查明错误行为并进行修复。

当我们在一个测试中断言多个行为时,我们无法清楚地了解哪些行为不起作用,因为测试只会报告第一个失败,其余的会被跳过。 要理解哪些更改是必要的以及有多少事情没有按预期工作变得更加困难。

命名测试

将好的测试与优秀的测试区分开来的一件事是测试名称。 测试不仅应该告诉我们它们做了什么,还应该告诉我们它们什么时候做。

您可以使用许多好的命名模式,因此请选择您认为最具描述性的一种并坚持使用。 以下是一些出色的测试名称示例:

  • RegistrationServiceShould.createNewAccountWhenEmailIsNotTaken
  • RegistrationServiceTest.whenEmailIsFree_createNewAccount
  • RegistrationServiceTest.if_freeEmail_when_userCreatesAccount_then_create

当您编写单元测试时,传达您正在测试的内容比遵循最佳方法命名实践更重要。 例如,在 Java 中,我们在编写方法时使用 camelCase,但使用下划线 (_) 将状态与测试名称中的操作分开是完全有效的。

清洁测试

您编写的测试应遵循您应用于代码的所有干净代码实践。 测试不是二等公民,您需要像处理其余代码一样小心谨慎,以使它们具有可读性。

测试中代码重复的定义非常重要。 DRY 原则(不要重复自己)适用于提取出于相同原因而发生变化的行为。 测试因不同的原因而改变,所以如果它们真的没有因为相同的原因而改变,那么从你的测试中提取东西时要有所不同。 剧透警报,他们通常不会。

if语句不属于测试。 if语句告诉我们,我们的测试至少做了两个不同的事情,如果我们将测试重写为两个不同的测试,我们会做得更好。 如果命名正确,将更容易理解测试的作用以及所有不同的行为。

我们什么时候应该写测试?

在 TDD 原则的指导下,我们应该在编写新代码之前编写测试。

当我们需要添加一个新特性时,我们首先将期望的行为描述为一个新的测试。 我们进行了最少的更改以通过该测试而不会破坏任何其他测试。

否则,时间越长,我们拥有的未经测试的代码就越多,引入错误或过度工程的机会就会增加

此外,测试变得更加复杂,因为我们现在有更多的代码要测试,通常发生的情况是开发人员将测试调整为代码。 这意味着我们调整行为以匹配我们的代码所做的事情,而不是相反。

当我们尽早编写测试时,问题的定义会变得更小,并且比更通用和更复杂的行为更容易解决这些问题。

最后的想法

虽然编写测试似乎是可选的事情,但从良好的基础开始至关重要。 编码已经很困难了,通过编写更易于阅读、理解和维护的可测试代码,让你自己和你的队友更轻松。 最后,如果您在使测试符合您的意愿时遇到困难,则问题更可能出现在您的代码中而不是您的测试中。

有兴趣编写干净且可测试的代码吗?
查看我们针对高级后端开发人员的空缺职位
或向我们发送开放申请