Sentry 的前端测试实践:从 Enzyme 迁移到 RTL

在 Sentry,我们采用了持续交付实践,也就是说,代码一旦被合并到主分支就可以立即发布。我们因此能够快速地迭代产品,尽可能频繁地在生产环境中提供新功能、进行错误修复、配置变更和实验。我们每月合并超过 700 个 PR。自动化测试——特别是我们大型的 React 代码库自动化测试——是我们 CI/CD 流程的重要组成部分,确保我们的产品可以按照预期运行。

 

在这篇文章中,我们将讨论如何将我们的组件测试从 Enzyme 迁移到 React Testing Library(RTL)。这项工作花费了一年零四个月的时间,有 17 名工程师参与,涉及 803 个测试套件和 4937 个测试,其中每一个测试都需要做出修改。

 

什么是 RTL

 

React Testing Library(RTL)是一个 React 组件测试库,它通过与 DOM 交互来测试组件,不像 Enzyme 那样直接使用了 React 内部的东西。RTL 是目前测试 React 组件最流行的方案,但 Sentry 在 RTL 出现之前就已经在使用 React 了。

 

故事开始

 

我们希望使用最新的现代化工具和实践,但不会因为它们是新的就一把梭哈。我们会仔细评估新技术,了解它们将给我们带来什么好处。当时我们已经知道有 RTL 了,但并没有强烈的理由要将它引入到我们的代码库中。我们用来测试组件库的 Enzyme 可以满足我们的需求。

 

2020 年,一些之前使用过 RTL 的工程师加入了 Sentry。基于他们使用 RTL 库的经验,他们向我们的前端技术指导委员会(Frontend TSC)提出了将 RTL 引入到我们的代码库中的建议。Frontend TSC 致力于 Sentry 的 JavaScript 代码库开发工作,他们每两周开会一次,为公司整体的前端开发团队讨论和总结最佳开发实践。

 

当时,我们已经在大规模地向 TypeScript 迁移,虽然迁移到 RTL 的想法听起来不错,而且我们也知道 Airbnb 已经将 Enzyme 的所有权转到了 GitHub,并且现在也在使用 RTL,但我们还没有为新的迁移做好准备。尽管如此,我们还是希望对此做一些概念验证(PoC),比较一下 RTL 和 Enzyme,并评估迁移将给我们带来哪些好处。我们对性能提升这一块特别感兴趣,因为我们当时的测试执行得非常慢。

 执行 Enzyme 测试通常需要很长时间,特别是如果你忘记在测试案例之间进行手动清理。上图是对单个组件的 Jest 测试结果。由于每个测试案例执行后缺少清理操作,每个测试的执行时间都会变长。

 

我们进行了 PoC,证明使用 RTL 会让测试执行稍微快一些。然而,12%左右的性能提升还不足以说服我们从 Enzyme 迁移到 RTL。但通过这个 PoC,我们注意到与 RTL 相比,Enzyme 有许多缺点,因为很多 Enzyme 测试没有测试可访问性,没有自动清理测试环境,并且经常直接访问组件状态。很快,我们发现在 Enzyme 中无法使用 React Hooks。

 

React 17 迫使我们采取行动

 

快进到 2021 年 4 月,TypeScript 迁移终于完成了。在我们的 TSC 会议上,团队成员经常会提到 RTL。通常,当有人需要修改组件的内部状态或样式,并发现还需要更新 Enzyme 测试(特别是快照)时,就会提到 RTL。这非常烦人,而且没有任何意义,因为这些变化不会影响用户在屏幕上看到的内容。

 

虽然优化开发工作流是迁移 RTL 的一个很好的理由,但在将 React 更新到 17 版本之前,我们仍然没有太多地关注这件事情。React 核心团队完全重写了 React 的内部,而 Enzyme 直接使用了许多 React 内部的东西。

 

React 17 带来的最大的内部改进之一是新的 JSX 转换。在 React 17 之前,你编写的 JSX 代码被转换成 React.createElement 调用。例如:

 

import React from 'react'




export function App() {
   return <h1>Hello World</h1>
}

 

被转换成:

 

import React from 'react'




export function App() {
  return React.createElement('h1', null, 'Hello World')
}

 

从 React 17 开始,你不再需要 import React from ‘react’了。React 包增加了两个新的入口点,这两个入口点原本仅供 Babel 或 TypeScript 等编译器使用。因此,对于上面的 JSX 代码,输出会略有不同:

 

import { jsx as _jsx } from 'react/jsx-runtime'




export function App() {
  return _jsx('h1', { children: 'Hello world' })
}

 

所以你只需要在 JSX 组件中这样写:

 

export function App() {
   return <h1>Hello World</h1>
}

 

Enzyme 无法百分之百与这个新版本的 React 协调,不过市面上出现了一个适配器可以解决这个问题,也就是我们在用的。然而,从长远来看,这个解决方案也不行,因为 React 18 会进行全面重写,而且考虑到 Airbnb 已经放弃对 Enzyme 的支持,这个解决方案是不会长久的。

 

从 Enzyme 迁移到 RTL 的好处现在更明了了:

 

  • Enzyme 没有支持 React 18 的计划。RTL 不依赖 React 内部的东西,并将继续支持 React 18,就像它支持 React 16 和和 React 17 一样。(注意:虽然有一个非官方的React 18适配器,但它并没有全面支持)。

  • Enzyme 只为测试 React Hooks 提供基本的支持。

  • 我们将不再需要使用适配器来测试 React。

  • RTL 提供的基于角色的选择器可以更好地进行可访问性测试。

  • RTL 不存在我们在使用 Enzyme 时遇到的一些陷阱,比如在测试案例之间不清理组件(速度慢)和直接修改组件状态(糟糕的测试实践)。

  • RTL 现在是在 npm 平台上测试 React 组件更受欢迎的选择,可能是因为 Enzyme 不直接支持最新版本的 React。

npm 上下载 RTL 和 Enzyme 的时间轴变化,RTL 在 2020 年中超过了 Enzyme

 

评估迁移时间

 

在 2021 年,我们已经承诺从 Enzyme 迁移到 RTL,并制定了一个已达成一致的策略来完成这项工作。我们想要知道实际需要做多少工作。是几周,一个月,还是一年?我们需要把这一辈子都花在转换这些文件上吗?我们需要了解这实际上将涉及多大的工作量,以便与我们的主要职责——Sentry 的开发做出协调。

 

对于这项工作,我们进行了几种评估,其中一个是“文件数量”评估,包含三个预测,第一个不太乐观,最后一个非常激进。我们的平均预测是每周 8 个文件,这是最准确的估计。这样一来,完工日期将在 2022 年底到 2023 年春季之间。

 

每周迁移 8 个文件

 

我们的迁移计划

 

我们刚刚成功地将 Sentry 的整个 JavaScript 代码库迁移到了 TypeScript。对于 RTL 迁移,我们决定采用类似的方法。

 

1.准备工作

 

我们为开发人员提供了 RTL 入门所必需的东西,并创建了一些通用的数据提供者,将测试元素与我们所需的 React 上下文和 Emotion CSS 主题提供者包装在一起。

 

2.教人们如何使用 RTL

 

在这个过渡时期,许多开发人员没有使用 RTL 编写测试的经验,所以我们提出了组织虚拟会议的想法,把大家聚在一起,尝试转换一些测试案例。这种方式非常有效,人们像发现新大陆一样兴奋,学习到了很多东西。这可能是我们作为一个团队一起做的最酷的事情之一。

 

此外,受 RTL 作者 Kent C. Dodds 所写的文章“使用RTL时要避免的常见错误”的启发,我们写了一个类似的最佳实践文档,可以持续记录我们试图遵循的最佳实践。

 

前端 TSC 成员还留出时间进行每日代码评审,这对于帮助那些希望按照最佳实践学习如何使用 RTL 的人来说是至关重要的。

 

3.用 RTL 编写所有新代码

 

在这个阶段,我们需要使用 RTL 编写所有的新测试。如果人们仍然用旧的风格编写测试,那就永远无法完成迁移。与之前的迁移一样,我们没有使用任何可以阻止人们用 Enzyme 编写新测试的工具。但是,我们确实做了一件事,就是在 Enzyme 函数和导入语句上使用了 JSDoc 的 @deprecated 标签,这样开发人员就会意识到不应该再使用它们了。

 

4.迭代转换旧代码

 

除了使用 RTL 编写新的测试,我们还有一定数量的文件需要转换,所以我们需要投入一些时间。我们还鼓励开发人员尽可能地参与文件转换,并在 sprint 中计划安排转换任务。

 

不可预见的挑战

 

在我们适应了 RTL 的思维模式后,在编写测试时一切都变得更有效率了。这个库提供的工具也很容易使用,但还是存在一些不可预见的挑战。

 

1.在大型组件上使用 getByRole 时性能较差

 

根据 RTL 的指南,测试应该像用户与组件交互的方式一样。基于这一原则,RTL 提供了谁都可以使用的查询和可以帮助我们以更容易访问的方式测试组件的语义查询。

 

其中的一个查询是 getByRole,用于查询可访问性树中公开的所有元素,根据 RTL 的指南,这个查询应该是我们的首选项。

 

尽管 getByRole 很有用,但我们发现这个查询的性能非常差。

 

使用 getByRole 选择器的大型组件可能会比较慢,我们的一个解决方法是避免多次调用这个查询,将它保存到一个变量中,或者切换使用 getByText 或 getByTestId。

 

2.在使用 userEvent.type 测试浏览器交互模拟时性能较差

 

根据 RTL 作者 Kent C. Dodds 的说法,我们应该使用 userEvent 而不是 fireEvent,并肯定 userEvent 更好,因为它提供了几个更接近浏览器交互的方法。

 

我们赞同 Kent 的建议,即触发事件和接近真实的用户体验会让测试变得更健壮。但是,在使用 userEvent.type 修改了一些测试后,我们发现一些测试执行超时,因为 Jest 的最长执行时间是 5 秒。

 

这是因为 userEvent.type 会模拟用户输入每一个字符时触发的每一个事件。虽然这是对的,也是我们想要的,但由于存在性能问题,我们已经在许多测试中将其替换为 userEvent.paste,并在一些地方使用 fireEvent。

 

3.在有很多样式化组件时使用 userEvent.click 性能较差

 

当 userEvent.click 被调用时,它使用 getComputedStyle 函数来确定被点击的元素是否可见以及指针事件不会禁用组件。JSDOM 实现了一个类似于在浏览器中运行的版本,但它会解析组件树中所有的样式化组件,直到被点击的元素。

 

如果元素嵌套很深,并且测试中包含了许多点击,可能会花费大量的时间重新计算样式。我们基于这个jsdom问题中提到的解决方案模拟了 window.getComputedStyle。我们通过这种方式用测试安全性换取了更好的性能。一个非常慢的测试从 94.93 秒下降到了 47.52 秒,这个测试涉及了多次单击、大量样式化组件和 react-select(也使用 getComputedStyle 放置下拉列表)。

 

4.元素 ARIA 角色的缺失迫使我们在可访问性方面下了很多功夫

 

我们的许多组件都没有合适的 ARIA 角色,我们不得不更新组件,以便像 RTL 建议的那样可以通过语义查询来选择它们。但是,有时候,我们不使用 ARIA 角色更新已弃用的组件,而是通过添加测试 ID 来更快地选择它们。

 

还有一些 ARIA 属性我们不是很熟悉,导致有时候我们会做出一些错误的假设,没有在元素上使用理想的 ARIA 标签,或者在引入它们时破坏了其他测试。

 

我们通过代码评审识别并修复了其中的许多问题。

 

5.转换重度测试组件内部的 Enzyme 测试案例

 

我们的一些测试会检查组件的状态,例如,如果加载状态被设置为 true,并且没有反映在 DOM 中,就不可能在不更新前端代码的情况下将这些逻辑转换为 RTL。

 

如果遇到这种情况,一些文件的转换就变得不那么简单了。但从另一个方面看,它帮助我们改善了用户体验,在 UI 上为我们提供了更多的反馈。

 

还有许多测试没有存在的意义,例如,一些事件图测试,所以我们决定将它们移除。

 

保持动力

 

因为有了之前的 TypeScript 迁移,我们也创建了一个 Slack 机器人(源代码),当它被激活时,它会告诉我们当前的迁移进度和剩余需要转换的文件数量。在转换的最后阶段,看到进度百分比每天都在上升对我们的团队来说是一个巨大的动力。

Slack 机器人用来跟踪从 Enzyme 到 RTL 的迁移进度

 

迁移结束时恰逢我们新财年的开始,因此许多开发人员放慢了转换文件的速度,优先考虑产品功能和季度计划。尽管每天只转换几个文件,我们仍然在不断取得进展,慢慢向目标靠近。

 

当我们进入最后阶段,眼看就要结束时,所有人又开始变得超级兴奋。

 

你可能还记得我们之前预测这将需要 1 年零 2 个月时间。到项目结束时,我们并没有偏离很多:在 1 年零 4 个月之后,我们使用 RTL 完成了 644 个文件转换,完成了我们的目标。

 

实际的迁移时间线

 

往后,TypeScript 无处不在

 

在迁移过程中,一些工程师将测试转换进一步推进,同时将测试转换为 TypeScript。TypeScript 与编辑器(如 VS Code)的语言服务器特性相结合,在添加代码时不断地提供实时提示,这有助于识别出传给组件的不必要或不正确的 props,并让用户体验变得更好。

 

迁移到 RTL 和 TypeScript 之前:

 

it('renders', function () {
    const wrapper = mountWithTheme(
      <ProjectCspReports
        organization={org}
        // 'project' doesn't exist in the component
        project={project} 
        {...TestStubs.routerProps({
          params: {orgId: org.slug, projectId: project.slug},
          location: TestStubs.location({pathname: routeUrl}),
        })}
      />
    );
    expect(wrapper).toSnapshot();
  });

 

迁移到 RTL 和 TypeScript 之后:

 

it('renders', function () {
    const {container} = render(
      <ProjectCspReports
        // the 'project' prop which was not required has been removed 
        route={{}}
        routeParams={router.params}
        router={router}
        routes={router.routes}
        location={TestStubs.location({pathname: routeUrl})}
        params={{orgId: organization.slug, projectId: project.slug}}
      />
    );
    expect(container).toSnapshot();
  });

 

在迁移过程中,我们还遇到了一个由一个新转换的 RTL 测试引起的错误,如果文件是用 TypeScript 编写的,这个错误是可以避免的。

 

因为对前端测试进行类型化对我们来说非常有好处,所以我们打算制定一个计划,将它们全部转换为 TypeScript。

 

总结

 

我们很高兴看到我们所有的前端测试都迁移到了 RTL!

 

尽管测试的执行性能并没有像我们所希望的那样得到大幅改善,但是 RTL 的引入为我们带来了许多其他好处,例如不再依赖于实现细节,而是尽可能多地测试用户看到和交互的内容,这些才是最重要的。

 

除了对测试的正确性充满了信心,我们也更有动力让我们的应用程序更容易访问,这符合我们的核心价值观之一——“为了每一位开发者”。

 

这次迁移是团队努力的结果。像这样的大型项目需要计划、努力、动机和大量的时间,并且所有这些都不能影响我们的产品目标。非常感谢每一位为这个项目的成功做出贡献的人,尤其是所有的工程师,尽管他们同时有其他许多事情要做,但总能抽出时间来转换测试。

原文链接:

https://blog.sentry.io/2023/02/23/sentrys-frontend-tests-migrating-from-enzyme-to-react-testing-library/

本文文字及图片出自 InfoQ

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注