摇摆不定的测试。摆脱测试中的恶梦

有一个寓言故事,这些天我经常想起。这则寓言是在我小时候告诉我的。它被称为伊索的 "狼来了的男孩"。它讲述了一个在村子里放羊的男孩。他觉得无聊,就假装有狼在袭击羊群,向村民们求救--但他们失望地发现这是一场虚惊,便不再理睬这个男孩。然后,当狼真的出现,男孩呼救时,村民们认为这又是一场虚惊,没有前来救援,羊群最终被狼吃掉了。

这个故事的寓意由作者本人作了最好的总结。

"骗子是不会被相信的,即使他说的是实话"。

狼袭击了羊群,男孩哭着求救,但经过无数次的谎言,已经没有人相信他了。这个寓意可以适用于测试。伊索的故事是一个很好的寓言,说明了我偶然发现的一个匹配模式:未能提供任何价值的片状测试。

前端测试。为什么这么麻烦?

我的大部分时间都花在了前端测试上。所以你不应该感到惊讶的是,本文中的代码例子大多来自我在工作中遇到的前端测试。然而,在大多数情况下,它们可以很容易地翻译成其他语言并应用于其他框架。所以,我希望这篇文章对你有用--不管你有什么专长。

值得回顾的是前端测试的含义。就其本质而言,前端测试是一套用于测试Web应用程序的用户界面的实践,包括其功能。

从质量保证工程师开始,我知道在发布前从检查表上无休止的手动测试的痛苦。因此,除了确保一个应用程序在连续的更新过程中保持无错误的目标之外,我还努力减轻那些你实际上不需要人做的常规任务所造成的测试工作量。现在,作为一个开发者,我发现这个话题仍然很有意义,特别是当我试图直接帮助用户和同事的时候。而在测试中,有一个问题特别让我们做噩梦。

片状测试的科学性

一个不稳定的测试是指每次运行相同的分析时都不能产生相同的结果。构建只是偶尔会失败。一次是通过,另一次是失败,下一次又是通过,没有对构建进行任何修改。

当我回忆起我的测试噩梦时,有一个案例特别出现在我的脑海中。那是在一个UI测试中。我们建立了一个自定义风格的组合框(即一个带有输入字段的可选择列表)。

通过这个组合框,你可以搜索一个产品并选择一个或多个结果。很多天,这个测试都很顺利,但在某些时候,事情发生了变化。在我们的持续集成(CI)系统的大约十个构建中的一个,在这个组合框中搜索和选择产品的测试失败了。

失败的截图显示结果列表没有被过滤,尽管搜索已经成功。

像这样的故障测试会阻碍持续部署管道,使功能交付比它需要的更慢。此外,一个不稳定的测试是有问题的,因为它不再是确定性的--使它变得无用。毕竟,你不会相信一个人,就像你不会相信一个骗子一样。

此外,故障测试的修复成本很高,通常需要几个小时甚至几天的时间来调试。尽管端到端测试更容易出现问题,但我在各种测试中都遇到过这种情况:单元测试、功能测试、端到端测试,以及两者之间的一切。

另一个重要的问题是他们给我们开发人员灌输的态度。当我开始从事测试自动化工作时,我经常听到开发人员在回应失败的测试时这样说。

"啊,那个构建。不要紧,再踢一次就好了。它最终会通过的,在某个时候"。

对我来说是一个巨大的红旗。它告诉我,构建中的错误不会被认真对待。有一种假设是,一个不稳定的测试不是一个真正的错误,而 "只是 "不稳定,不需要被照顾,甚至不需要被调试。反正以后测试会再次通过,对吗?不是的!如果这样的提交被合并,在最坏的情况下,我们的产品中会有一个新的不稳定的测试。

原因

所以,不稳定的测试是有问题的。我们应该怎么做呢?好吧,如果我们知道问题所在,我们可以设计一个反策略。

我在日常生活中经常遇到这样的原因。它们可以在测试本身中找到。测试可能写得不太理想,持有错误的假设,或者包含不好的做法。然而,不仅仅是这样。摇摆不定的测试可能是更糟糕的迹象。

在下面的章节中,我们将讨论我所遇到的最常见的问题。

1.测试方面的原因

在一个理想的世界里,你的应用程序的初始状态应该是纯洁的,100%可预测的。在现实中,你永远不知道你在测试中使用的ID是否会一直是相同的。

让我们检查一下我的两个失败的例子。第一个错误是在我的测试夹具中使用一个ID

{
   "id": "f1d2554b0ce847cd82f3ac9bd1c0dfca",
   "name": "Variant product",
}

第二个错误是在UI测试中寻找一个独特的选择器,然后想,"好吧,这个ID看起来很独特。我将使用它"。

<!-- This is a text field I took from a project I worked on -->
<input type="text" id="sw-field--f1d2554b0ce847cd82f3ac9bd1c0dfca" />

然而,如果我在另一个装置上运行这个测试,或者后来在CI的几个构建中运行,那么这些测试可能会失败。我们的应用程序将重新生成ID,在不同的构建中改变它们。所以,第一个可能的原因是在硬编码的ID中找到的。

第二个原因可能来自随机(或其他)生成的演示数据。当然,你可能认为这个 "缺陷 "是合理的--毕竟,数据的生成是随机的--但想想调试这个数据。要看清一个错误是在测试本身还是在演示数据中,可能非常困难。

接下来是我无数次纠结过的测试端原因:有交叉依赖的测试。一些测试可能无法独立运行或以随机顺序运行,这是有问题的。此外,以前的测试可能会干扰到后面的测试。这些情况会通过引入副作用而导致测试的不稳定。

然而,不要忘记,测试是对假设的挑战。如果你的假设一开始就有缺陷,会发生什么?我经常经历这些,我最喜欢的是关于时间的有缺陷的假设。

一个例子是使用不准确的等待时间,特别是在UI测试中--例如,通过使用固定的等待时间。下面这句话取自Nightwatch.js的测试。

// Please never do that unless you have a very good reason!
// Waits for 1 second
browser.pause(1000);

另一个错误的假设与时间本身有关。我曾经发现,一个不稳定的PHPUnit测试只在我们的夜间构建中失败。经过一些调试,我发现昨天和今天的时间转换是罪魁祸首。另一个很好的例子是由于时区而导致的失败。

错误的假设并不止于此。我们也可能对数据的顺序有错误的假设。想象一下,一个包含多个条目信息的网格或列表,比如一个货币列表。

我们想处理第一个条目的信息,即 "捷克克朗 "货币。你能确定你的应用程序在每次执行测试时都会把这段数据作为第一条吗?会不会在某些情况下,"欧元 "或其他货币会成为第一个条目?

不要假设你的数据会按照你需要的顺序出现。类似于硬编码的ID,在不同的构建中,顺序可能会改变,这取决于应用程序的设计。

2.环境方面的原因

下一类原因与你的测试之外的一切有关。具体来说,我们谈论的是执行测试的环境,测试之外的CI和docker相关的依赖--所有这些你几乎无法影响的东西,至少在你作为测试员的角色中是这样。

一个常见的环境方面的原因是资源泄漏。通常情况下,这将是一个应用程序的负载,导致不同的加载时间或意外的行为。大型测试很容易造成泄漏,吃掉大量的内存。另一个常见的问题是缺乏清理

依赖关系之间的不兼容尤其让我做噩梦。一个噩梦发生在我使用Nightwatch.js进行UI测试时。Nightwatch.js使用WebDriver,这当然依赖于Chrome。当Chrome冲刺更新时,出现了兼容性的问题。Chrome、WebDriver和Nightwatch.js本身不再一起工作,这导致我们的构建时常失败。

说到依赖性值得一提的是任何npm的问题,比如权限丢失或npm停机。我在观察CI的过程中经历了所有这些问题。

当涉及到环境问题导致的UI测试错误时,请记住,你需要整个应用程序堆栈,以便它们能够运行。涉及的东西越多,出错的可能性就越大。因此,JavaScript测试是网络开发中最难稳定的测试,因为它们涵盖了大量的代码。

3.产品方面的原因

最后但同样重要的是,我们真的要小心这第三个领域--一个有实际bug的领域。我说的是产品方面的原因,即松散性。最著名的例子之一是应用程序中的竞赛条件。当这种情况发生时,这个错误需要在产品中修复,而不是在测试中修复在这种情况下,试图修复测试或环境是没有用的。

对抗软弱性的方法

我们已经确定了导致松散性的三个原因。我们可以在此基础上建立我们的反击策略!当然,当你遇到不稳定的测试时,牢记这三个原因,你已经收获颇丰。你已经知道应该寻找什么以及如何改进测试。然而,除此之外,还有一些策略可以帮助我们设计、编写和调试测试,我们将在下面的章节中一起看一下。

关注你的团队

你的团队可以说是最重要的因素。作为第一步,承认你有一个测试不稳定的问题。获得整个团队的承诺是至关重要的然后,作为一个团队,你需要决定如何处理不稳定的测试。

在我从事技术工作的这些年里,我遇到了四个团队用来对付不稳定的策略。

  1. 什么都不做,接受不稳定的测试结果。
    当然,这个策略根本就不是一个解决方案。测试不会产生任何价值,因为你不能再相信它了--即使你接受它的缺陷。所以我们可以很快跳过这个问题。
  2. 这个策略在我职业生涯的开始阶段很常见,导致了我前面提到的反应。有一些人接受重试测试直到它们通过。这种策略不需要调试,但它很懒惰。除了隐藏问题的症状外,它还会使你的测试套件更加缓慢,这使得该解决方案不可行。然而,这个规则可能有一些例外,我将在后面解释。
  3. 删除并忘记测试。
    这个是不言自明的。简单地删除不稳定的测试,这样它就不会再干扰你的测试套件。当然,这将节省你的钱,因为你不需要再调试和修复测试。但它的代价是失去了一点测试覆盖率和失去潜在的错误修复。测试的存在是有原因的!不要通过删除测试来射杀信使。
  4. 隔离和修复。
    我在这个策略上取得了最大的成功。在这种情况下,我们会暂时跳过测试,并让测试套件不断提醒我们有一个测试被跳过。为了确保修复工作不被忽视,我们会在下一个冲刺阶段安排一个任务。机器人提醒也很有效。一旦导致浮动的问题被修复,我们将再次整合(即取消跳过)测试。不幸的是,我们会暂时失去覆盖率,但它会随着修复而回来,所以这不会花很长时间。

这些策略帮助我们处理工作流层面的测试问题,而且我不是唯一遇到过这些问题的人。在他的文章中,Sam Saffron也得出了类似的结论。但在我们的日常工作中,它们对我们的帮助是有限的。那么,当这样的任务来到我们身边时,我们该如何进行呢?

保持测试隔离

在计划你的测试用例和结构时,始终保持你的测试与其他测试隔离,这样它们就能以独立或随机的顺序运行。最重要的步骤是在测试之间恢复一个干净的安装。此外,只测试你想测试的工作流程,并且只为测试本身创建模拟数据。这个捷径的另一个好处是,它可以提高测试性能。如果你遵循这些要点,就不会有其他测试的副作用或遗留的数据妨碍你。

下面的例子来自于一个电子商务平台的UI测试,它涉及到客户在店面的登录。该测试是用JavaScript编写的,使用的是Cypress框架)。

// File: customer-login.spec.js
let customer = {};

beforeEach(() => {
    // Set application to clean state
    cy.setInitialState()
      .then(() => {
        // Create test data for the test specifically
        return cy.setFixture('customer');
      })
}):

第一步是将应用程序重置为一个干净的安装。这是作为beforeEach 生命周期钩子的第一步,以确保每次都能执行重置。之后,专门为测试创建测试数据--对于这个测试案例,将通过一个自定义命令创建一个客户。随后,我们可以从我们想要测试的一个工作流开始:客户的登录。

进一步优化测试结构

我们可以做一些其他的小调整,使我们的测试结构更加稳定。第一个是很简单的。从小型测试开始。如前所述,你在测试中做的越多,可能出错的就越多。尽量保持测试的简单,避免在每个测试中出现大量的逻辑。

当涉及到不假设数据的顺序时(例如,在UI测试中处理列表中的条目顺序时),我们可以设计一个测试来独立于任何顺序的功能。回到网格中信息的例子,我们不会使用伪选择器或其他对顺序有强烈依赖性的CSS。我们可以使用文本或其他对顺序无所谓的东西来代替nth-child(3) 选择器。例如,我们可以使用这样的断言:"在这个表中给我找一个有这一个文本字符串的元素"。

等等!测试重试有时是可以的?

重试测试是一个有争议的话题,而且是理所当然的。我仍然认为,如果测试被盲目地重试直到成功,那是一种反模式。然而,有一个重要的例外。当你无法控制错误时,重试可以是最后的手段(例如,排除来自外部依赖的错误)。在这种情况下,我们不能影响错误的来源。然而,这样做时要格外小心。在重试测试时,不要对松散性视而不见,当一个测试被跳过时,要使用通知来提醒你。

下面的例子是我在我们的CI中使用的GitLab的例子。其他环境可能有不同的语法来实现重试,但这应该能让你体会到。

test:
    script: rspec
    retry:
        max: 2
        when: runner_system_failure

在这个例子中,我们要配置的是,如果工作失败,应该进行多少次重试。有趣的是,如果运行器系统中出现错误(例如,作业设置失败),可以重试。我们选择只在docker设置失败的情况下重试我们的作业。

注意,这将在触发时重试整个作业。如果你希望只重试有问题的测试,那么你需要在你的测试框架中寻找一个功能来支持这个。下面是一个来自Cypress的例子,它从第5版开始就支持单个测试的重试。

{
    "retries": {
        // Configure retry attempts for 'cypress run`
        "runMode": 2,
        // Configure retry attempts for 'cypress open`
        "openMode": 2,
    }
}

你可以在Cypress的配置文件中激活测试重试,cypress.json 。在那里,你可以在测试运行器和无头模式中定义重试的尝试。

使用动态等待时间

这一点对所有类型的测试都很重要,但尤其是UI测试。我怎么强调都不为过。永远不要使用固定的等待时间--至少在没有很好的理由的情况下不要。如果你这样做,请考虑可能的结果。在最好的情况下,你会选择过长的等待时间,使测试套件比它需要的更慢。在最坏的情况下,你将不会等待足够长的时间,所以测试不会进行,因为应用程序还没有准备好,导致测试以一种不稳定的方式失败。根据我的经验,这是导致测试不稳定的最常见原因。

相反,使用动态等待时间。有许多方法可以做到这一点,但Cypress处理得特别好。

所有的Cypress命令都拥有一个隐含的等待方法。它们已经检查了该命令所应用的元素是否在DOM中存在指定的时间--指向Cypress的重试能力。然而,它只检查是否存在,仅此而已。所以我建议更进一步--等待你的网站或应用程序的用户界面中的任何变化,而这些变化也是真实的用户会看到的,比如用户界面本身或动画中的变化。

这个例子在元素上使用了一个明确的等待时间,选择器为.offcanvas 。只有当该元素在指定的超时之前是可见的,测试才会进行,你可以配置这个超时。

// Wait for changes in UI (until element is visible)
cy.get(#element).should('be.visible');

在Cypress中,动态等待的另一个巧妙的可能性是其网络功能。是的,我们可以等待请求的发生,并等待其响应的结果。我特别经常使用这种等待。在下面的例子中,我们定义了要等待的请求,使用一个wait 命令来等待响应,并断言其状态代码。

// File: checkout-info.spec.js

// Define request to wait for
cy.intercept({
    url: '/widgets/customer/info',
    method: 'GET'
}).as('checkoutAvailable');

// Imagine other test steps here...

// Assert the response’s status code of the request
cy.wait('@checkoutAvailable').its('response.statusCode')
  .should('equal', 200);

这样一来,我们就可以根据我们的应用需要精确地等待,使测试更加稳定,不容易因为资源泄漏或其他环境问题而出现故障。

调试不稳定的测试

我们现在知道了如何通过设计来防止测试失灵。但是,如果你已经在处理一个不稳定的测试了呢?你怎么能摆脱它呢?

当我在调试的时候,把有缺陷的测试放在一个循环中,对我发现易碎性有很大帮助。例如,如果你运行一个测试50次,而且每次都能通过,那么你就可以更确定这个测试是稳定的--也许你的修复措施起了作用。如果不是,你至少可以更深入地了解这个不稳定的测试。

// Use in build Lodash to repeat the test 100 times
Cypress._.times(100, (k) => {
    it(`typing hello ${k + 1} / 100`, () => {
        // Write your test steps in here
    })
})

在CI中,获得对这种不稳定测试的更多了解尤其艰难。为了获得帮助,看看你的测试框架是否能够获得更多关于你的构建的信息。当涉及到前端测试时,你通常可以在你的测试中利用一个console.log 。

it('should be a Vue.JS component', () => {
    // Mock component by a method defined before
    const wrapper = createWrapper();


    // Print out the component’s html
    console.log(wrapper.html());

    expect(wrapper.isVueInstance()).toBe(true);
})

这个例子取自一个Jest单元测试,我在其中使用了一个console.log ,以获得被测试组件的HTML输出。如果你在Cypress的测试运行器中使用这种记录的可能性,你甚至可以在你选择的开发者工具中检查输出。此外,当涉及到CI中的Cypress时,你可以通过使用一个插件在你的CI的日志中检查这个输出。

始终关注你的测试框架的功能,以获得对日志的支持。在UI测试中,大多数框架都提供截图功能--至少在失败时,会自动进行截图。有些框架甚至提供视频记录,这对深入了解测试中发生的情况有很大帮助。

对抗虚弱的噩梦!

重要的是,要不断地寻找故障测试,无论是从一开始就防止它们发生,还是在它们发生后立即进行调试和修复。我们需要认真对待它们,因为它们可以暗示你的应用程序中的问题。

识别红旗

当然,最好是在第一时间内防止故障测试的发生。快速回顾一下,这里有一些红旗。

  • 测试是大型的,包含很多逻辑。
  • 测试涵盖了大量的代码(例如,在UI测试中)。
  • 测试使用了固定的等待时间。
  • 测试依赖于以前的测试。
  • 该测试断言的数据不是100%可预测的,如使用ID、时间或演示数据,特别是随机生成的数据。

如果你牢记本文的指针和策略,你就可以在测试发生之前防止闪失。如果它们真的来了,你将知道如何调试和修复它们。

这些步骤确实帮助我恢复了对我们测试套件的信心。目前,我们的测试套件似乎很稳定。未来可能会有问题 - 没有什么是100%完美的。这些知识和这些策略将帮助我处理它们。因此,我将对自己对抗那些片状测试噩梦的能力越来越有信心。

我希望我至少能够减轻你的一些痛苦和对片状物的担忧!

进一步阅读

如果你想进一步了解这个话题可以加我q3177181324,我可以给你发很好的资源和文章,它们对我帮助很大,希望对你们也有帮助。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值