加入直播:美国东部时间周四上午 11 点,Storybook 9 版本发布及 AMA

测试工具,允许您在单元测试中重用您的故事

在 Github 上查看

⚠️ 注意!

如果您正在使用 Storybook 7,您需要阅读本节。否则,请随意跳过。

@storybook/testing-react 已升级为 Storybook 的一级功能。这意味着**您不再需要此包**。相反,您可以从 @storybook/react 包中导入相同的工具。此外,composeStoriescomposeStory 的内部结构已得到改进,因此故事的组合方式更加准确。@storybook/testing-react 包将被弃用,因此我们建议您进行迁移。

请执行以下操作

  1. 卸载此包
  2. 更新您的导入
- import { composeStories } from '@storybook/testing-react';
+ import { composeStories } from '@storybook/react';

// OR
- import { setProjectAnnotations } from '@storybook/testing-react';
+ import { setProjectAnnotations } from '@storybook/react';

问题

您正在使用 Storybook 开发组件,并使用 jest 为其编写测试,很可能同时使用 EnzymeReact testing library。在您的 Storybook 故事中,您已经定义了组件的场景。您也设置了必要的装饰器(主题、路由、状态管理等),以使它们都能正确渲染。在编写测试时,您最终也会定义组件的场景,并设置必要的装饰器。重复做相同的事情让您觉得付出了太多精力,使得编写和维护故事/测试变得不那么有趣,更像是一种负担。

解决方案

@storybook/testing-react 提供了一种解决方案,可以在您的 React 测试中重用您的 Storybook 故事。通过在测试中重用您的故事,您就拥有了一个随时可测试的组件场景目录。您的args装饰器,来自您的故事及其 meta,以及全局装饰器,都将由这个库组合起来,并以一个简单的组件形式返回给您。这样,在您的单元测试中,您只需选择要渲染哪个故事,所有必要的设置都将为您完成。这是弥合测试编写和 Storybook 故事编写之间差距的关键,从而实现更好的共享性和维护性。

安装

此库应作为您项目的 devDependencies 之一进行安装

通过 npm

npm install --save-dev @storybook/testing-react

或通过 yarn

yarn add --dev @storybook/testing-react

设置

Storybook 6 和组件故事格式

此库要求您使用 Storybook 版本 6、组件故事格式 (CSF)提升的 CSF 注解,这是自 Storybook 6 以来推荐的故事编写方式。

总的来说,如果您使用 Storybook 6 并且您的故事看起来与此类似,那么您就可以开始了!

// CSF: default export (meta) + named exports (stories)
export default {
  title: 'Example/Button',
  component: Button,
};

const Primary = args => <Button {...args} />; // or with Template.bind({})
Primary.args = {
  primary: true,
};

全局配置

这是可选步骤。如果您没有全局装饰器,则无需执行此操作。但是,如果您有,这是应用全局装饰器的必要步骤。

如果您有全局装饰器/参数等,并希望在测试故事时应用它们,您首先需要进行设置。您可以通过添加或创建一个 jest 设置文件来完成此操作

// setupFile.js <-- this will run before the tests in jest.
import { setProjectAnnotations } from '@storybook/testing-react';
import * as globalStorybookConfig from './.storybook/preview'; // path of your preview.js file

setProjectAnnotations(globalStorybookConfig);

为了让 jest 识别到设置文件,您需要在测试命令中将其作为选项传递给 jest

// package.json
{
  "test": "react-scripts test --setupFiles ./setupFile.js"
}

用法

composeStories

composeStories 将处理您指定的组件中的所有故事,在所有故事中组合 args/decorators,并返回一个包含组合故事的对象。

如果您使用组合故事(例如 PrimaryButton),组件将使用故事中传递的 args 进行渲染。但是,您可以自由地在组件之上传递任何 props,这些 props 将覆盖故事 args 中传递的默认值。

import { render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories'; // import all stories from the stories file

// Every component that is returned maps 1:1 with the stories, but they already contain all decorators from story level, meta level and global level.
const { Primary, Secondary } = composeStories(stories);

test('renders primary button with default args', () => {
  render(<Primary />);
  const buttonElement = screen.getByText(
    /Text coming from args in stories file!/i
  );
  expect(buttonElement).not.toBeNull();
});

test('renders primary button with overriden props', () => {
  render(<Primary>Hello world</Primary>); // you can override props and they will get merged with values from the Story's args
  const buttonElement = screen.getByText(/Hello world/i);
  expect(buttonElement).not.toBeNull();
});

composeStory

如果您希望仅应用于单个故事而不是所有故事,可以使用 composeStory。您还需要传递 meta(默认导出)。

import { render, screen } from '@testing-library/react';
import { composeStory } from '@storybook/testing-react';
import Meta, { Primary as PrimaryStory } from './Button.stories';

// Returns a component that already contain all decorators from story level, meta level and global level.
const Primary = composeStory(PrimaryStory, Meta);

test('onclick handler is called', () => {
  const onClickSpy = jest.fn();
  render(<Primary onClick={onClickSpy} />);
  const buttonElement = screen.getByRole('button');
  buttonElement.click();
  expect(onClickSpy).toHaveBeenCalled();
});

将项目注解设置为 composeStorycomposeStories

setProjectAnnotations 旨在应用您在 .storybook/preview.js 文件中定义的所有全局配置。这意味着如果您的 preview.js 导入了某些模拟或其他您实际上不希望在测试文件中执行的内容,您可能会遇到意想不到的副作用。如果出现这种情况,而您仍然需要提供一些通常来自 preview.js 的注解覆盖(装饰器、参数等),您可以将它们直接作为 composeStoriescomposeStory 函数的可选最后一个参数传递。

composeStories:

import * as stories from './Button.stories'

// default behavior: uses overrides from setProjectAnnotations
const { Primary } = composeStories(stories)

// custom behavior: uses overrides defined locally
const { Primary } = composeStories(stories, { decorators: [...], globalTypes: {...}, parameters: {...})

composeStory:

import * as stories from './Button.stories'

// default behavior: uses overrides from setProjectAnnotations
const Primary = composeStory(stories.Primary, stories.default)

// custom behavior: uses overrides defined locally
const Primary = composeStory(stories.Primary, stories.default, { decorators: [...], globalTypes: {...}, parameters: {...})

重用故事属性

composeStoriescomposeStory 返回的组件不仅可以作为 React 组件渲染,还附带了故事、meta 和全局配置的组合属性。这意味着如果您想访问 argsparameters 等,您可以这样做

import { render, screen } from '@testing-library/react';
import { composeStory } from '@storybook/testing-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.children);
});

CSF3

Storybook 6.4 发布了 新版本的 CSF,其中故事也可以是一个对象。@storybook/testing-react 支持此功能,但您必须满足以下条件之一

1 - 您的故事有一个 render 方法 2 - 或者您的 meta 有一个 render 方法 3 - 或者您的 meta 包含一个 component 属性

// Example 1: Meta with component property
export default {
  title: 'Button',
  component: Button, // <-- This is strictly necessary
};

// Example 2: Meta with render method:
export default {
  title: 'Button',
  render: args => <Button {...args} />,
};

// Example 3: Story with render method:
export const Primary = {
  render: args => <Button {...args} />,
};

与 play 函数的交互

Storybook 6.4 还带来了名为 play 的新函数,您可以在其中为故事编写自动化交互。

@storybook/testing-react 中,play 函数不会自动为您运行,而是包含在返回的组件中,您可以根据需要执行它。

考虑以下示例

export const InputFieldFilled: Story<InputFieldProps> = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.type(canvas.getByRole('textbox'), 'Hello world!');
  },
};

您可以像这样使用 play 函数

const { InputFieldFilled } = composeStories(stories);

test('renders with play function', async () => {
  const { container } = render(<InputFieldFilled />);

  // pass container as canvasElement and play an interaction that fills the input
  await InputFieldFilled.play({ canvasElement: container });

  const input = screen.getByRole('textbox') as HTMLInputElement;
  expect(input.value).toEqual('Hello world!');
});

批量测试文件中的所有故事

您不必手动指定每个测试,还可以结合使用 test.eachcomposeStories 来运行自动化测试。以下是一个对文件中的所有故事进行快照测试的示例

import * as stories from './Button.stories';

const testCases = Object.values(composeStories(stories)).map((Story) => [
  // The ! is necessary in Typescript only, as the property is part of a partial type
  Story.storyName!,
  Story,
]);
// Batch snapshot testing
test.each(testCases)('Renders %s story', async (_storyName, Story) => {
  const tree = await render(<Story />);
  expect(tree.baseElement).toMatchSnapshot();
});

Typescript

@storybook/testing-react 支持 Typescript,并提供自动补全功能,轻松检测组件的所有故事

component autocompletion

它还提供了组件的 props,就像您在测试中直接使用它们时通常期望的那样

props autocompletion

类型推断仅在 tsconfig.json 文件中将 strictstrictBindApplyCall 模式设置为 true 的项目中才可能实现。您还需要 TypeScript 版本在 4.0.0 以上。如果您没有正确的类型推断,这可能是原因所在。

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "strict": true, // You need either this option
    "strictBindCallApply": true // or this option
    // ...
  }
  // ...
}

免责声明

为了让类型自动被识别,您的故事必须是带类型的。请看示例

import React from 'react';
import { Story, Meta } from '@storybook/react';

import { Button, ButtonProps } from './Button';

export default {
  title: 'Components/Button',
  component: Button,
} as Meta;

// Story<Props> is the key piece needed for typescript validation
const Template: Story<ButtonProps> = args => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  children: 'foo',
  size: 'large',
};

许可证

MIT

由...制作
  • shaunlloyd
    shaunlloyd
  • kylegach
    kylegach
  • tooppaaa
    tooppaaa
  • ndelangen
    ndelangen
  • shilman
    shilman
  • alexandrebodin
    alexandrebodin
标签