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

过去十年,Web UI 技术突飞猛进。尽管如此,在 2024 年构建/维护生产级 UI 比以往任何时候都更加困难。
在 Storybook,我们与全球数千家顶尖 UI 团队合作,包括 Microsoft、Supabase 和 JPMorganChase 等公司。无论团队大小,无论最终结果多么完善,我们都看到他们在管理复杂前端开发方面面临着类似的挑战。
许多团队希望对 UI 进行测试覆盖率以捕获回归,但他们无法承担维护大型端到端测试套件的成本(我们将在下面详细探讨)。与此同时,他们通常拥有数千个单元测试,但这些测试在 Node 中使用模拟浏览器环境运行,并不能让他们对 UI 产生太大的信心。
在一次又一次地看到相同的模式后,我们正在押注组件测试是 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 等工具也会渲染和测试组件,但在实际浏览器中进行。这些测试就是我们定义的组件测试。
因此,组件测试
- 在浏览器中渲染组件以实现高保真度
- 像 E2E 测试一样模拟用户与实际 UI 的交互
- 仅测试 UI 的一个单元(例如,单个组件),并且可以深入到实现细节中进行模拟或操作数据,就像单元测试一样
组件测试:完美的补充
如上所述,组件测试兼具单元测试和 E2E 测试的元素。但为什么组件测试有用,何时应该使用它们?
让我们从一个简单的论断开始
E2E 测试是保真度最高的测试,因为它们精确地测试了用户在使用您的应用程序时所看到的内容。
在没有其他考虑因素的情况下,如果您可以用 E2E 测试来测试 UI 的某个特定功能,那么您就应该这样做。E2E 测试让您对一切协同工作最有信心。
但是,如果 E2E 如此出色(它确实如此!),为什么许多团队却吝啬使用它呢?
关键在于“其他考虑因素”。尽管 E2E 测试取得了长足的进步,但实际的限制使其难以对 UI 的每个方面进行 E2E 测试。
挑战包括
- 测试运行速度慢且容易出现不稳定
- 大量“难以触及”的状态
- 设置和测试后端需要大量开销
- 黑盒环境只能从外部进行操作
所有这些挑战都可以通过组件测试来解决,但代价是未能测试整个系统。
这使得这两种技术成为完美的补充
- E2E 测试可以覆盖您应用程序中少量的主要流程
- 组件测试可以覆盖范围更广的其他重要 UI 状态
而这正是我们认为 UI 应该被测试的方式。

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

E2E 测试
此应用程序的主要流程是从主页开始,导航到一家餐厅,将商品添加到购物车,然后结账。
我们使用 Chromatic 在 Playwright 中实现了这个流程,对沿途的每个步骤进行视觉测试。该测试导航到每个页面,并在每个状态下拍摄 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('https://: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!');
});
这个单一的测试涵盖了单个流程中的各种状态,模拟了用户在应用程序中的实际体验。在配备 16GB 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 个组件中有很大一部分的测试覆盖率达到了 100%,基于上面展示的 story。
此外,这些测试运行得非常快。在配备 16GB RAM 的 2021 MacBook M1 Pro 上,浏览器中运行的 89 个测试的整个套件需要 8-10 秒,这几乎与上面运行单个 E2E 测试所需的时间差不多。
立即试用
Storybook 8.2 支持组件测试。在新项目中尝试一下
npx storybook@latest init
或升级现有项目
npx storybook@latest upgrade
有关本文所示的完整示例,请参阅Mealdrop 存储库。要了解更多信息,请参阅 Storybook 的组件测试文档。
下一步是什么?
端到端 (E2E) 测试功能强大,因为它们如同用户所见一样测试您的应用程序。但是,由于测试不稳定、执行速度以及其他实际考虑因素,编写和维护大量 E2E 测试以覆盖复杂应用程序中的所有关键 UI 状态非常具有挑战性。组件测试提供了完美的补充,这就是为什么我们全力以赴将 Storybook 打造成组件测试的强大工具。
在接下来的几个月里,我们将在 Storybook 中发布各种 UI 测试改进。这些更改包括
- 使 Storybook 的 story 格式与其他测试工具保持一致。
- 与 Vitest 合作实现闪电般的测试执行速度。
- 能够从 Storybook 的 UI 中运行测试并查看结果。
- 将多种类型的测试结合在一次运行中,包括功能测试、视觉测试、可访问性测试等。
- 一种独特的方式,可以无缝调试开发和 CI 中的测试失败。
有关我们正在考虑和积极开发的项目概述,请查看 Storybook 的路线图。
🆕 Storybook 中的组件测试
— Storybook (@storybookjs) 2024 年 8 月 29 日
我们正在押注组件测试是 UI 测试的未来。
请继续阅读以了解更多关于是什么、为什么以及如何做…… pic.twitter.com/4nQcqB7AqB