Picasso:如何测试一个组件库
Toptal的设计系统最近发布了一个新版本,这需要我们对Picasso(我们的内部组件库)中的几乎所有组件进行修改。我们的团队面临着一个挑战:我们如何确保退步不会发生?
简短的答案是,毫不奇怪,测试。大量的测试。
我们不会回顾测试的理论方面,也不会讨论不同类型的测试,它们的作用,或者解释为什么你应该首先测试你的代码。我们的博客和其他博客已经涵盖了这些主题。相反,我们将只专注于测试的实践方面。
请继续阅读,了解Toptal的开发人员如何编写测试。我们的资源库是公开的,所以我们使用真实世界的例子。没有任何抽象或简化的东西。
测试金字塔
我们没有定义测试金字塔本身,但如果我们有的话,它看起来像这样。
Toptal的测试金字塔说明了我们强调的测试。
单元测试
单元测试是直接编写和容易运行的。如果你只有很少的时间来写测试,他们应该是你的第一选择。
然而,他们并不完美。无论你选择哪个测试库(在我们的例子中是Jest和React测试库[RTL]),它都不会有真正的DOM,也不能让你在不同的浏览器中检查功能,但它可以让你剥离复杂性,测试你的库的简单构建块。
单元测试不只是通过测试代码的行为来增加价值,而且还通过检查代码的整体可测试性。如果你不能轻松地编写单元测试,那么你的代码很可能是坏的。
视觉回归测试
即使你有100%的单元测试覆盖率,这也不意味着组件在不同的设备和浏览器上看起来很好。
视觉回归在人工测试中特别难以发现。例如,如果一个按钮的标签移动了1px,QA工程师会注意到吗?值得庆幸的是,有很多解决方案可以解决这个有限的可见性问题。你可以选择企业级的一体式解决方案,如LambdaTest或Mabl。你可以将插件,如Percy,纳入你现有的测试,以及来自Loki或Storybook(这是我们在Picasso之前使用的)这样的DIY解决方案。它们都有缺点。有的太贵,有的学习曲线太陡峭,有的需要太多维护。
Happo拯救了我们!它是Percy的直接竞争对手,但它要便宜得多,支持更多的浏览器,而且更容易使用。另一个大卖点?它支持Cypress集成,这很重要,因为我们想摆脱使用Storybook进行视觉测试。我们发现,我们不得不创建故事,以确保视觉测试的覆盖率,而不是因为我们需要记录该用例。这污染了我们的文档,使它们更难理解。我们希望将可视化测试与可视化文档隔离开来。
集成测试
即使两个组件有单元和视觉测试,也不能保证它们能一起工作。例如,我们发现了一个错误,当在一个下拉项目中使用时,工具提示不能打开,但在单独使用时却能很好地工作。
为了确保组件集成良好,我们使用了Cypress的实验性组件测试功能。起初,我们对糟糕的性能感到不满,但我们能够通过一个自定义的webpack配置来改善它。结果呢?我们能够使用Cypress优秀的API来编写高性能的测试,确保我们的组件能够很好地协同工作。
应用测试金字塔
这一切在现实生活中是什么样子的?让我们来测试一下Accordion组件!
你的第一直觉可能是打开你的编辑器,开始写代码。我的建议是?花一些时间了解该组件的所有功能,并写下你想涵盖的测试案例。
要测试什么?
下面是我们的测试应该涵盖的案例的分类。
- 状态- Accordions可以被展开和折叠,它的默认状态可以被配置,并且这个功能可以被禁用。
- 样式- 手风琴可以有边框变化
- 内容--它们可以与库中的其他单元集成。
- 自定义--该组件可以重写其样式,可以有自定义的展开图标。
- 回调- 每当状态改变时,可以调用一个回调。
如何测试?
现在我们知道我们要测试什么,让我们考虑如何去做。从我们的测试金字塔来看,我们有三个选择。我们希望在金字塔的各个部分之间实现最大的覆盖率,并使其重叠最小。测试每个测试用例的最好方法是什么?
- 状态- 单元测试可以帮助我们评估状态是否有相应的变化,但我们也需要视觉测试,以确保组件在每个状态下都能正确呈现。
- 样式- 视觉测试应该足以检测不同变体的回归情况
- 内容- 视觉测试和集成测试的结合是最好的选择,因为Accordions可以与许多其他组件结合使用。
- 定制- 我们可以使用单元测试来验证类名是否被正确应用,但我们需要一个视觉测试来确保组件和自定义样式的协同工作。
- 回调- 单元测试是确保正确的回调被调用的理想选择。
手风琴测试的金字塔
单元测试
完整的单元测试套件可以在这里找到。我们已经涵盖了所有的状态变化、定制和回调。
it('toggles', async () => {
const handleChange = jest.fn()
const { getByText, getByTestId } = renderAccordion({
onChange: handleChange,
expandIcon: <span data-testid='trigger' />
})
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)
})
视觉回归测试
视觉测试位于这个Cypress描述块中。截图可以在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描述块中写了一个 "坏路径 "测试,断言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每次都要经历的阶段。
- 依赖关系的安装
- 版本控制- 验证提交的格式和PR的标题是否符合传统的 提交方式
- Lint- ESlint确保良好的代码质量
- TypeScript 编译- 验证是否有类型错误
- 包的编译--如果包不能被构建,那么它们就不会被成功发布;我们的Cypress测试也希望有编译过的代码
- 单元测试
- 集成和视觉测试
完整的工作流程可以在这里找到。目前,完成所有阶段的工作只需要不到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的工程师和与之交互的终端用户。用户关心的是良好的UI/UX,而开发人员则需要能够在他们的应用程序和你的组件之间编写集成测试。作为一个维护者,你必须满足这两方面的需求。
什么是视觉回归测试?
改变一行代码可能会意外地影响到你没有想到的功能(回归)。但有时,回归只是视觉上的;用户界面仍然工作,但看起来不一样了(视觉回归)。单元测试或E2E测试不会抓到这个错误,但视觉回归测试应该抓到。
为什么视觉回归测试很重要?
在不同的版本之间保持UI的一致性对用户体验很重要。大多数自动化测试不与UI交互,他们以编程方式操作它。他们不会注意到一个按钮是否改变了颜色,或者一个图片是否溢出了页面。但用户会注意到,视觉回归测试也会注意到。