Docs
Storybook Docs

单元中的故事测试

团队使用不同的工具测试各种 UI 特性。每个工具都要求你一遍又一遍地复制相同的组件状态。这是一个维护难题。理想情况下,你会以类似的方式设置测试并在工具之间重复使用它。

¥Teams test a variety of UI characteristics using different tools. Each tool requires you to replicate the same component state over and over. That’s a maintenance headache. Ideally, you’d set up your tests similarly and reuse that across tools.

Storybook 使你能够隔离组件并将其用例捕获到 *.stories.js|ts 文件中。故事是标准的 JavaScript 模块,与整个 JavaScript 生态系统交叉兼容。

¥Storybook enables you to isolate a component and capture its use cases in a *.stories.js|ts file. Stories are standard JavaScript modules that are cross-compatible with the whole JavaScript ecosystem.

故事是 UI 测试的实用起点。将故事导入 Jest测试库VitestPlaywright 等工具,以节省时间和维护工作。

¥Stories are a practical starting point for UI testing. Import stories into tools like Jest, Testing Library, Vitest and Playwright, to save time and maintenance work.

使用测试库编写测试

¥Write a test with Testing Library

测试库 是一套用于基于浏览器的组件测试的辅助库。使用 组件故事格式,你的故事可以通过测试库重复使用。每个命名导出(故事)都可以在你的测试设置中渲染。例如,如果你正在开发登录组件并想要测试无效凭据场景,你可以按以下方式编写测试:

¥Testing Library is a suite of helper libraries for browser-based component tests. With Component Story Format, your stories are reusable with Testing Library. Each named export (story) is renderable within your testing setup. For example, if you were working on a login component and wanted to test the invalid credentials scenario, here's how you could write your test:

Storybook 提供了一个 composeStories 实用程序,可帮助将测试文件中的故事转换为可渲染元素,这些元素可以在使用 JSDOM 的 Node 测试中重复使用。它还允许你将已在项目中启用的其他 Storybook 功能(例如 decoratorsargs)应用到你的测试中,使你能够在你选择的测试环境中重复使用你的故事(例如 JestVitest),确保你的测试始终与你的故事同步,而无需重写它们。这就是我们在 Storybook 中所说的可移植故事。

¥Storybook provides a composeStories utility that helps convert stories from a test file into renderable elements that can be reused in your Node tests with JSDOM. It also allows you to apply other Storybook features that you have enabled your project (e.g., decorators, args) into your tests, enabling you to reuse your stories in your testing environment of choice (e.g., Jest, Vitest), ensuring your tests are always in sync with your stories without having to rewrite them. This is what we refer to as portable stories in Storybook.

Form.test.ts|tsx
import { fireEvent, render, screen } from '@testing-library/react';
 
import { composeStories } from '@storybook/react';
 
import * as stories from './LoginForm.stories'; // 👈 Our stories imported here.
 
const { InvalidForm } = composeStories(stories);
 
test('Checks if the form is valid', async () => {
  // Renders the composed story
  await InvalidForm.run();
 
  const buttonElement = screen.getByRole('button', {
    name: 'Submit',
  });
 
  fireEvent.click(buttonElement);
 
  const isFormValid = screen.getByLabelText('invalid-form');
  expect(isFormValid).toBeInTheDocument();
});

你必须 配置你的测试环境使用可移植的故事 来确保你的故事由 Storybook 配置的所有方面组成,例如 decorators

¥You must configure your test environment to use portable stories to ensure your stories are composed with all aspects of your Storybook configuration, such as decorators.

测试运行后,它会加载故事并渲染它。测试库 然后模拟用户的行为并检查组件状态是否已更新。

¥Once the test runs, it loads the story and renders it. Testing Library then emulates the user's behavior and checks if the component state has been updated.

覆盖故事属性

¥Override story properties

默认情况下,setProjectAnnotations 函数会将你在 Storybook 实例中定义的任何全局配置(即 preview.js|ts 文件中的参数、装饰器)注入到你现有的测试中。尽管如此,这可能会对不打算使用这些全局配置的测试造成无法预料的副作用。例如,你可能希望始终在特定区域(通过 globalTypes)中测试故事,或者配置故事以应用特定的 decoratorsparameters

¥By default, the setProjectAnnotations function injects into your existing tests any global configuration you've defined in your Storybook instance (i.e., parameters, decorators in the preview.js|ts file). Nevertheless, this may cause unforeseen side effects for tests that are not intended to use these global configurations. For example, you may want to always test a story in a particular locale (via globalTypes) or configure a story to apply specific decorators or parameters.

为了避免这种情况,你可以通过扩展 composeStorycomposeStories 函数来覆盖全局配置,以提供特定于测试的配置。例如:

¥To avoid this, you can override the global configurations by extending either the composeStory or composeStories functions to provide test-specific configurations. For example:

Form.test.js|ts
// Replace your-renderer with the renderer you are using (e.g., react, vue3, svelte, etc.)
import { composeStories } from '@storybook/your-renderer';
 
import * as stories from './LoginForm.stories';
 
const { ValidForm } = composeStories(stories, {
  decorators: [
    // Decorators defined here will be added to all composed stories from this function
  ],
  globalTypes: {
    // Override globals for all composed stories from this function
  },
  parameters: {
    // Override parameters for all composed stories from this function
  },
});

在单个故事上运行测试

¥Run tests on a single story

你可以使用 composeStory 函数允许你的测试在单个故事上运行。但是,如果你依赖这种方法,我们建议你将故事元数据(即 默认导出)提供给 composeStory 函数。这可确保你的测试可以准确确定有关故事的正确信息。例如:

¥You can use the composeStory function to allow your tests to run on a single story. However, if you're relying on this method, we recommend that you supply the story metadata (i.e., the default export) to the composeStory function. This ensures that your tests can accurately determine the correct information about the story. For example:

Form.test.ts|tsx
import { fireEvent, screen } from '@testing-library/react';
 
import { composeStory } from '@storybook/react';
 
import Meta, { ValidForm as ValidFormStory } from './LoginForm.stories';
 
const ValidForm = composeStory(ValidFormStory, Meta);
 
test('Validates form', async () => {
  await ValidForm.run();
 
  const buttonElement = screen.getByRole('button', {
    name: 'Submit',
  });
 
  fireEvent.click(buttonElement);
 
  const isFormValid = screen.getByLabelText('invalid-form');
  expect(isFormValid).not.toBeInTheDocument();
});

将故事合并为一个测试

¥Combine stories into a single test

如果你打算在单个测试中测试多个故事,请使用 composeStories 函数。它将处理你指定的每个组件故事,包括你定义的任何 argsdecorators。例如:

¥If you intend to test multiple stories in a single test, use the composeStories function. It will process every component story you've specified, including any args or decorators you've defined. For example:

Form.test.ts|tsx
import { fireEvent, screen } from '@testing-library/react';
 
import { composeStories } from '@storybook/react';
 
import * as FormStories from './LoginForm.stories';
 
const { InvalidForm, ValidForm } = composeStories(FormStories);
 
test('Tests invalid form state', async () => {
  await InvalidForm.run();
 
  const buttonElement = screen.getByRole('button', {
    name: 'Submit',
  });
 
  fireEvent.click(buttonElement);
 
  const isFormValid = screen.getByLabelText('invalid-form');
  expect(isFormValid).toBeInTheDocument();
});
 
test('Tests filled form', async () => {
  await ValidForm.run();
 
  const buttonElement = screen.getByRole('button', {
    name: 'Submit',
  });
 
  fireEvent.click(buttonElement);
 
  const isFormValid = screen.getByLabelText('invalid-form');
  expect(isFormValid).not.toBeInTheDocument();
});

故障排除

¥Troubleshooting

在其他框架中运行测试

¥Run tests in other frameworks

Storybook 为其他框架(如 Vue 2Angular)提供社区主导的插件。但是,这些插件仍然不支持最新的稳定 Storybook 版本。如果你有兴趣提供帮助,我们建议使用默认沟通渠道(GitHub 和 Discord 服务器)联系维护者。

¥Storybook provides community-led addons for other frameworks like Vue 2 and Angular. However, these addons still lack support for the latest stable Storybook release. If you're interested in helping out, we recommend reaching out to the maintainers using the default communication channels (GitHub and Discord server).

参数未传递给测试

¥The args are not being passed to the test

composeStoriescomposeStory 返回的组件不仅可以渲染为 React 组件,还可以带有来自故事、元和全局配置的组合属性。这意味着,例如,如果你想访问参数或参数,你可以这样做:

¥The components returned by composeStories or composeStory not only can be rendered as React components but also come with the combined properties from the story, meta, and global configuration. This means that if you want to access args or parameters, for instance, you can do so:

Button.test.ts|tsx
import { render, screen } from '@testing-library/react';
 
import { composeStories } from '@storybook/react';
 
import * as stories from './Button.stories';
 
const { Primary } = composeStories(stories);
 
test('reuses args from composed story', () => {
  render(<Primary />);
 
  const buttonElement = screen.getByRole('button');
  // Testing against values coming from the story itself! No need for duplication
  expect(buttonElement.textContent).toEqual(Primary.args.label);
});

了解其他 UI 测试

¥Learn about other UI tests