
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 测试取得了很大进展,但仍然存在一些实际限制,使得 E2E 测试 UI 的每个方面都具有挑战性。
挑战包括
- 较慢的测试运行,容易出现不稳定性
- 许多“难以到达”的状态
- 设置和测试后端的开销
- 黑盒环境只能从外部进行操作
所有这些挑战都可以通过组件测试来解决,但代价是不测试整个系统。
这使得这两种技术成为完美的补充
- E2E 测试可以覆盖应用程序中的少量 happy path
- 组件测试可以覆盖更广泛的其他重要 UI 状态
而这正是我们认为应该如何测试 UI 的方式。

Mealdrop 示例应用程序
为了使这个命题具体化,让我们考虑 Mealdrop,一个实现食品配送服务的示例项目

E2E 测试
此应用程序的 happy path 从主页开始,导航到餐厅,将商品添加到购物车,然后结账。
我们已在 Playwright 中实现了此流程,并使用 Chromatic 对沿途的每个步骤进行可视化测试。该测试导航到每个页面,并在每个状态下拍摄 UI 的可视化快照,以确保页面正确渲染。它还在沿途对 DOM 进行了一些关键断言。为了简洁起见,测试在下面进行了压缩;完整测试可在 Mealdrop repo 中找到。
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% 的测试覆盖率。
此外,这些测试运行速度非常快。在配备 16G RAM 的 2021 MacBook M1 Pro 上,整个 89 个测试套件在浏览器中运行仅需 8-10 秒,这仅仅比运行上面的单个 E2E 测试稍长一点。
立即尝试
Storybook 8.2 支持组件测试。在新项目中尝试一下
npx storybook@latest init
或者升级现有项目
npx storybook@latest upgrade
对于本文中显示的完整示例,请参阅 Mealdrop repo。要了解更多信息,请参阅 Storybook 的组件测试文档。
下一步是什么?
端到端 (E2E) 测试功能强大,因为它们完全按照用户看到的方式测试您的应用程序。但是,由于测试不稳定、执行速度和其他实际考虑因素,编写和维护大量必要的 E2E 测试来覆盖复杂应用程序中的所有关键 UI 状态具有挑战性。组件测试提供了完美的补充,这就是为什么我们全力以赴将 Storybook 变成组件测试强者的原因。
在接下来的几个月中,我们将在 Storybook 中发布各种 UI 测试改进。这些更改包括
- 使 Storybook 的 story 格式与其他测试工具达到同等水平。
- 与 Vitest 合作实现闪电般的快速测试执行。
- 能够从 Storybook 的 UI 运行测试并查看结果。
- 在单次运行中结合多种类型的测试,包括功能测试、可视化测试、a11y 测试等。
- 一种在开发和 CI 中无缝调试测试失败的独特方法。
要了解我们正在考虑和积极开展的项目的概览,请查看 Storybook 的路线图。
🆕 Storybook 中的组件测试
— Storybook (@storybookjs) 2024年8月29日
我们押注组件测试是 UI 测试的未来。
继续阅读以了解更多关于内容、原因和方式… pic.twitter.com/4nQcqB7AqB