Picasso:如何测试组件库

已发表: 2022-03-11

最近发布了 Toptal 设计系统的新版本,它要求我们对 Picasso(我们的内部组件库)中的几乎每个组件进行更改。 我们的团队面临着一个挑战:我们如何确保不会发生回归?

毫不奇怪,简短的回答是测试。 很多测试。

我们不会回顾测试的理论方面,也不会讨论不同类型的测试、它们的用处,或者解释为什么你应该首先测试你的代码。 我们的博客和其他人已经涵盖了这些主题。 相反,我们将只关注测试的实际方面。

继续阅读以了解 Toptal 的开发人员如何编写测试。 我们的存储库是公开的,因此我们使用真实世界的示例。 没有任何抽象或简化。

测试金字塔

我们本身并没有定义测试金字塔,但如果我们这样做了,它看起来像这样:

测试金字塔图

Toptal 的测试金字塔说明了我们强调的测试。

单元测试

单元测试易于编写且易于运行。 如果您几乎没有时间编写测试,那么它们应该是您的首选。

然而,它们并不完美。 无论您选择哪个测试库(在我们的例子中是 Jest 和 React 测试库 [RTL]),它都没有真正的 DOM,并且不允许您在不同的浏览器中检查功能,但它允许您剥离消除复杂性并测试库的简单构建块。

单元测试不仅通过测试代码的行为来增加价值,而且还通过检查代码的整体可测试性来增加价值。 如果您不能轻松编写单元测试,那么您的代码可能很糟糕。

视觉回归测试

即使你有 100% 的单元测试覆盖率,这并不意味着组件在设备和浏览器中看起来都很好。

手动测试特别难以发现视觉回归。 例如,如果按钮的标签移动了 1px,QA 工程师会注意到吗? 值得庆幸的是,对于这个可见性有限的问题,有很多解决方案。 您可以选择企业级一体化解决方案,例如 LambdaTest 或 Mabl。 您可以将 Percy 之类的插件合并到您现有的测试中,以及 Loki 或 Storybook 之类的 DIY 解决方案(这是我们在 Picasso 之前使用的)。 它们都有缺点:有些过于昂贵,而另一些则学习曲线陡峭或需要太多维护。

八宝来救援! 它是 Percy 的直接竞争对手,但价格便宜得多,支持更多浏览器,并且更易于使用。 另一个大卖点? 它支持 Cypress 集成,这很重要,因为我们希望摆脱使用 Storybook 进行可视化测试。 我们发现自己必须创建故事以确保可视化测试覆盖率,而不是因为我们需要记录该用例。 这污染了我们的文档,使它们更难理解。 我们想将可视化测试与可视化文档隔离开来。

集成测试

即使两个组件都有单元测试和可视化测试,也不能保证它们可以一起工作。 例如,我们发现了一个错误,即工具提示在下拉项目中使用时无法打开,但在单独使用时效果很好。

为确保组件良好集成,我们使用了赛普拉斯的实验性组件测试功能。 起初,我们对性能不佳感到不满,但我们能够通过自定义 webpack 配置对其进行改进。 结果? 我们能够使用赛普拉斯出色的 API 编写高性能测试,确保我们的组件能够很好地协同工作。

应用测试金字塔

这一切在现实生活中是什么样子的? 让我们测试 Accordion 组件!

您的第一反应可能是打开编辑器并开始编写代码。 我的建议? 花一些时间了解组件的所有功能并写下您想要涵盖的测试用例。

Picasso 组件库演示 GIF

测试什么?

以下是我们的测试应涵盖的案例细分:

  • 状态- 手风琴可以展开和折叠,可以配置其默认状态,并且可以禁用此功能
  • 样式- 手风琴可以有边框变化
  • 内容——它们可以与图书馆的其他单元集成
  • 自定义– 组件的样式可以被覆盖,并且可以有自定义展开图标
  • 回调- 每次状态更改时,都可以调用回调

Picasso 组件库演示 GIF - 手风琴组件

如何测试?

现在我们知道我们必须测试什么,让我们考虑如何去做。 我们的测试金字塔有三个选项。 我们希望在金字塔各部分之间重叠最小的情况下实现最大覆盖。 测试每个测试用例的最佳方法是什么?

  • 状态- 单元测试可以帮助我们评估状态是否相应改变,但我们还需要视觉测试来确保组件在每个状态下都正确呈现
  • 样式——视觉测试应该足以检测不同变体的回归
  • 内容——可视化和集成测试的组合是最佳选择,因为 Accordions 可以与许多其他组件结合使用
  • 自定义——我们可以使用单元测试来验证类名是否正确应用,但我们需要一个可视化测试来确保组件和自定义样式协同工作
  • 回调——单元测试非常适合确保调用正确的回调

手风琴测试金字塔

单元测试

完整的单元测试套件可以在这里找到。 我们已经介绍了所有状态更改、自定义和回调:

 it('toggles', async () => { const handleChange = jest.fn() const { getByText, getByTestId } = renderAccordion({ onChange: handleChange, expandIcon: <span data-test /> }) fireEvent.click(getByTestId('accordion-summary')) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) fireEvent.click(getByTestId('trigger')) await waitFor(() => expect(getByText(DETAILS_TEXT)).not.toBeVisible()) fireEvent.click(getByText(SUMMARY_TEXT)) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) expect(handleChange).toHaveBeenCalledTimes(3) })

视觉回归测试

视觉测试位于此赛普拉斯描述块中。 屏幕截图可以在 Happo 的仪表板中找到。

您可以看到所有不同的组件状态、变体和自定义都已记录。 每次打开 PR 时,CI 都会将 Happo 存储的屏幕截图与在您的分支中截取的屏幕截图进行比较:

 it('renders', () => { mount( <TestingPicasso> <TestAccordion /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders disabled', () => { mount( <TestingPicasso> <TestAccordion disabled /> <TestAccordion expandIcon={<Check16 />} /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders border variants', () => { mount( <TestingPicasso> <TestAccordion borders='none' /> <TestAccordion borders='middle' /> <TestAccordion borders='all' /> </TestingPicasso> ) cy.get('body').happoScreenshot() })

集成测试

我们在这个 Cypress describe 块中编写了一个“坏路径”测试,它断言 Accordion 仍然可以正常工作,并且用户可以与自定义组件进行交互。 我们还添加了视觉断言以增加信心:

 describe('Accordion with custom summary', () => { it('closes and opens', () => { mount(<AccordionCustomSummary />) toggleAccordion() getAccordionContent().should('not.be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() toggleAccordion() getAccordionContent().should('be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() }) // … })

持续集成

Picasso 几乎完全依赖 GitHub Actions 进行 QA。 此外,我们为暂存文件的代码质量检查添加了 Git 挂钩。 我们最近从 Jenkins 迁移到 GHA,所以我们的设置仍处于 MVP 阶段。

工作流按顺序在远程分支中的每个更改上运行,集成和可视化测试是最后一个阶段,因为它们的运行成本最高(无论是在性能还是金钱成本方面)。 除非所有测试都成功完成,否则无法合并拉取请求。

这些是 GitHub Actions 每次都会经历的阶段:

  1. 依赖安装
  2. 版本控制——验证提交的格式和 PR 标题是否与常规提交匹配
  3. Lint – ESlint 确保高质量的代码
  4. TypeScript 编译——验证没有类型错误
  5. 包编译——如果无法编译包,则无法成功发布; 我们的赛普拉斯测试也期望编译代码
  6. 单元测试
  7. 集成和视觉测试

完整的工作流程可以在这里找到。 目前,完成所有阶段只需不到 12 分钟。

可测试性

像大多数组件库一样,Picasso 有一个根组件,它必须包装所有其他组件,并且可以用来设置全局规则。 这使得编写测试变得更加困难,原因有两个——测试结果的不一致,取决于包装器中使用的道具; 和额外的样板:

 import { render } from '@testing-library/react' describe('Form', () => { it('renders', () => { const { container } = render( <Picasso loadFavicon={false} environment='test'> <Form /> </Picasso> ) expect(container).toMatchSnapshot() }) })

我们通过创建一个以全局规则为测试的前提条件的 TestingPicasso 解决了第一个问题。 但是必须为每个测试用例声明它很烦人。 这就是为什么我们创建了一个自定义渲染函数,它将传递的组件包装在一个 TestingPicasso 中,并返回 RTL 渲染函数中可用的所有内容。

我们的测试现在更容易阅读和编写:

 import { render } from '@toptal/picasso/test-utils' describe('Form', () => { it('renders', () => { const { container } = render(<Form />) expect(container).toMatchSnapshot() }) })

结论

这里描述的设置远非完美,但对于那些有足够冒险精神来创建组件库的人来说,这是一个很好的起点。 我读过很多关于测试金字塔的文章,但在实践中应用它们并不总是那么容易。 因此,我邀请您探索我们的代码库并从我们的错误和成功中学习。

组件库是独一无二的,因为它们服务于两种受众:与 UI 交互的最终用户和使用您的代码构建自己的应用程序的开发人员。 在强大的测试框架上投入时间将使每个人受益。 在可测试性改进上投入时间将使您作为维护者和使用(和测试)您的库的工程师受益。

我们没有讨论诸如代码覆盖率、端到端测试以及版本和发布政策之类的事情。 关于这些主题的简短建议是:经常发布,练习适当的语义版本控制,在您的流程中保持透明度,并为依赖您的库的工程师设定期望。 我们可能会在后续文章中更详细地重新讨论这些主题。