
Storybook 中的组件测试
UI 测试的未来

在过去十年中,Web UI 技术取得了突飞猛进的发展。尽管如此,在2024年构建/维护一个生产级别的 UI 比以往任何时候都更困难。
在 Storybook,我们与全球数千家顶级 UI 团队合作,其中包括 Microsoft、Supabase 和 JPMorganChase 等公司。无论团队规模大小,或最终成果多么精美,我们都看到他们在管理复杂前端开发方面面临着类似的挑战。
许多团队希望对他们的 UI 进行测试覆盖以捕获回归错误,但他们无法承担维护大型端到端测试套件的成本(我们将在下面更详细地探讨)。同时,他们通常拥有数千个单元测试,但这并没有给他们带来太多 UI 信心,因为这些测试在 Node 中使用模拟的浏览器环境运行。
在反复看到相同的模式后,我们将组件测试视为 UI 测试的未来方向。
组件测试在浏览器中渲染一个 UI 组件,独立于应用程序的其余部分。它还可以与组件交互并进行断言。
组件测试抓住了 UI 测试的要害,它提供了类似端到端测试的浏览器保真度,同时具有单元测试的速度、可靠性和紧凑性。
组件测试不是端到端测试或单元测试的替代品,而是完美的补充。请继续阅读,了解更多关于组件测试的信息,它如何融入更广泛的测试领域,以及为什么我们认为它非常适合您的大多数 UI 测试。
什么是组件测试?
如果您在过去十年中一直在 JavaScript 生态系统中进行开发,您可能已经见过这样的单元测试(由 Testing Library 提供)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Fetch from './fetch';
it('loads and displays greeting', async () => {
// ARRANGE
render(<Fetch url="/greeting" />);
// ACT
await userEvent.click(
screen.getByText('Load Greeting')
);
await screen.findByRole('heading');
// ASSERT
expect(screen.getByRole('heading'))
.toHaveTextContent('hello there');
expect(screen.getByRole('button')).toBeDisabled();
});
该测试渲染了一个名为 Fetch
的组件,通过 DOM 与其交互,然后根据该交互断言 DOM 的变化。
它有点像端到端 (E2E) 测试,因为它模拟用户与应用程序 UI 的交互。组件可以小到一个按钮,也可以大到整个应用程序页面,而且在层级树中越往上,它就越像 E2E 测试。
但它不是 E2E 测试,因为它是在与应用程序其余部分隔离的环境中测试单个组件。这种差异既是组件测试的优势,也是劣势,具体取决于您想要测试的内容,正如我们将在下面看到的那样。
此外,此示例 (Jest + Testing Library) 在 Node 中运行,基于 JSDom 等 DOM 模拟层。由于它仅在浏览器模拟环境中运行,因此在 JSDom 中通过的测试可能在实际场景中失败,反之亦然。
Storybook、Vitest、Playwright、Cypress、Webdriver 和 Nightwatch 等工具也渲染并测试组件,但它们是在实际浏览器中运行。这些测试就是我们定义的组件测试。
因此,组件测试
- 在浏览器中渲染组件以实现高保真度
- 模拟用户与实际 UI 的交互,类似于 E2E 测试
- 仅测试 UI 的一个单元(例如单个组件),并且可以深入到实现细节进行模拟或数据操作,类似于单元测试
组件测试:完美的补充
正如我们在上面所见,组件测试兼具单元测试和 E2E 测试的元素。但组件测试为何有用,以及何时应该使用它们?
让我们从一个简单的论断开始
E2E 测试是保真度最高的测试,因为它们准确地测试用户在使用您的应用程序时会看到的内容。
在没有其他考虑的情况下,如果您可以使用 E2E 测试来测试您的 UI 的特定功能,那么您就应该这样做。E2E 测试能给您最大的信心,确保一切都按预期协同工作。
但是,如果 E2E 测试如此出色(确实如此!),为什么许多团队很少使用它们呢?
症结在于“其他考虑”。尽管 E2E 测试取得了许多进展,但仍存在一些实际限制,使得对 UI 的各个方面进行 E2E 测试变得具有挑战性。
挑战包括
- 测试运行速度较慢,容易出现不稳定
- 许多“难以触达”的状态
- 设置和测试后端所需的开销
- 黑盒环境只能从外部进行操作
所有这些挑战都可以通过组件测试解决,代价是不测试整个系统。
这使得这两种技术成为完美的补充
- E2E 测试可以覆盖应用中的少量正常流程
- 组件测试可以覆盖广泛的其他重要 UI 状态
这正是我们认为应该测试 UI 的方式。

Mealdrop 示例应用
为了使这个观点具体化,让我们考虑 Mealdrop,这是一个实现送餐服务的示例项目

E2E 测试
通过此应用的正常流程始于主页,导航到一家餐厅,将商品添加到购物车,然后结账。
我们使用 Playwright 实现了此流程,并使用 Chromatic 对每一步进行视觉测试。测试导航到每个页面,并在每个状态下获取 UI 的视觉快照,以确保页面正确渲染。它还在过程中对 DOM 进行了一些关键断言。为了简洁起见,下方是精简后的测试;完整的测试可在 Mealdrop 仓库中找到。
import { test, expect } from '@playwright/test';
test('should complete the full user journey from home to success page', async ({ page }) => {
await page.goto('http://localhost:3000');
// Navigate to Restaurants page
await page.getByText('View all restaurants').click();
// Select "Burgers" category
await page.getByText('Burgers').click();
// Select the first restaurant from the list
const restaurantCards = await page.getAllByTestId('restaurant-card');
await restaurantCards.first().click();
// Add Cheeseburger to the cart
const foodItem = await page.getByText(/Cheeseburger/i);
await foodItem.click();
// Go to "Checkout" page
await page.getByText(/checkout/i).click();
// Fill in order details...
// Complete the order
await page.getByRole('button', { name: 'Complete order' }).click();
await expect(page.locator('h1')).toContainText('Order confirmed!');
});
这一个测试覆盖了单个流程中的多种状态,模拟了用户在应用中的实际体验。在配备 16G RAM 的 2021 款 MacBook M1 Pro 上运行需要 5-6 秒。
但是,仍有多种状态未被覆盖,例如加载和错误状态、表单验证检查等等。为了覆盖它们,我们可以添加更多 E2E 测试,让它们通过应用的不同路径,并有意触发我们遗漏的状态。但是,根据我们上面的论点,我们选择使用组件测试来覆盖这些状态。
组件测试
为了覆盖遗漏的状态,我们使用 Storybook 进行组件测试。每个 Story 都是一段小代码片段,用于将组件配置到关键的 UI 状态。让我们来看看 Mealdrop 的 RestaurantDetailPage
组件的几个 Story。
最简单的 Story Success
几乎不像一个测试。它通过使用 Mock Service Worker 模拟 RestaurantDetailPage
组件使用的数据来执行冒烟测试,并验证组件是否成功渲染
// RestaurantDetailPage.stories.tsx
import { Meta, StoryObj } from '@storybook/react';
import { http, HttpResponse } from 'msw';
import { expect } from '@storybook/test';
import { BASE_URL } from '../../api';
import { restaurants } from '../../stub/restaurants';
import { RestaurantDetailPage } from './RestaurantDetailPage';
const meta = {
component: RestaurantDetailPage,
// All stories render the component and a spot to render the modal
render: () => {
return (
<>
<RestaurantDetailPage />
<div id="modal" />
</>
);
},
} satisfies Meta<typeof RestaurantDetailPage>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Success = {
parameters: {
// Mock data dependency
msw: {
handlers: [
http.get(BASE_URL, () => HttpResponse.json(restaurants[0])),
],
},
},
} satisfies Story;

但 Story 也可以使用 play
函数与浏览器交互并断言其内容。例如,WithModalOpen
单击餐厅的一个菜单项,并验证结果模态框是否存在于 DOM 中
// RestaurantDetailPage.stories.tsx
export const WithModalOpen = {
...Success,
play: async ({ canvas, userEvent }) => {
const item = await canvas.findByText(/Cheeseburger/i);
await userEvent.click(item);
await expect(canvas.getByTestId('modal')).toBeInTheDocument();
},
} satisfies Story;

最后,我们可以模拟网络请求以模拟访问错误,例如这个 404 NotFound
Story
// RestaurantDetailPage.stories.tsx
export const NotFound = {
parameters: {
msw: {
handlers: [
// Mock a 404 response
http.get(BASE_URL, () => HttpResponse.json(null, { status: 404 })),
],
},
},
play: async ({ canvas }) => {
const item = await canvas.findByText(/We can't find this page/i);
await expect(item).toBeInTheDocument();
},
} satisfies Story;

与将整个应用程序作为黑盒交互的 E2E 测试不同,组件测试可以自由地模拟或监视技术栈中的任何层级,具体取决于作者的需求。
因此,可以触达任何 UI 状态——这在 E2E 测试中可能非常具有挑战性。Mealdrop 的 45 个组件中的大多数都基于如上所示的 Story 实现了 100% 的测试覆盖率。
此外,这些测试运行速度非常快。整个 89 个测试套件在配备 16G RAM 的 2021 款 MacBook M1 Pro 上的浏览器中运行需要 8-10 秒,这仅比运行上面的单个 E2E 测试稍长。
立即尝试
Storybook 8.2 支持组件测试。在新项目中尝试
npx storybook@latest init
或升级现有项目
npx storybook@latest upgrade
本文所示的完整示例,请参见Mealdrop 仓库。要了解更多信息,请参阅 Storybook 的组件测试文档。
下一步是什么?
端到端 (E2E) 测试功能强大,因为它们精确地测试用户看到的应用程序。但由于测试不稳定、执行速度慢以及其他实际考虑,编写和维护覆盖复杂应用中所有关键 UI 状态所需的大量 E2E 测试具有挑战性。组件测试提供了完美的补充,这就是为什么我们全力以赴将 Storybook 打造成组件测试的主力军。
在接下来的几个月里,我们将在 Storybook 中推出各种 UI 测试改进。这些变化包括
- 使 Storybook 的 Story 格式与其他测试工具保持一致。
- 与 Vitest 合作实现闪电般的测试执行速度。
- 从 Storybook 的 UI 中运行测试并查看结果的功能。
- 在单次运行中组合多种类型的测试,包括功能测试、视觉测试、无障碍测试等。
- 一种独特的方式,可以在开发环境和 CI 中无缝调试测试失败。
要概览我们正在考虑和积极进行的项目,请查看 Storybook 的路线图。
🆕 Storybook 中的组件测试
— Storybook (@storybookjs) 2024年8月29日
我们将组件测试视为 UI 测试的未来方向。
继续阅读以了解更多关于是什么、为什么以及如何…… pic.twitter.com/4nQcqB7AqB