允許您在單元測試中重複使用 Story 的測試工具

在 Github 上查看

⚠️ 注意!

如果您使用的是 Storybook 7,則需要閱讀此章節。否則,請隨意跳過。

@storybook/testing-react 已被提升為 Storybook 的一級功能。這表示 您不再需要這個套件。相反地,您可以從 @storybook/react 套件匯入相同的工具。此外,composeStoriescomposeStory 的內部機制已進行改進,因此 story 的組成方式更加準確。@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 story 中,您已經定義了元件的各種情境。您也設定了必要的裝飾器(主題、路由、狀態管理等),以確保它們都能正確渲染。當您編寫測試時,您也最終會定義元件的情境,並設定必要的裝飾器。重複執行相同的操作兩次,您會覺得自己花費了太多精力,使得編寫和維護 story/測試變得不像是一種樂趣,而更像是一種負擔。

解決方案

@storybook/testing-react 是一個在您的 React 測試中重複使用 Storybook story 的解決方案。透過在您的測試中重複使用 story,您將擁有一個隨時可以測試的元件情境目錄。來自您的 args裝飾器story 及其 meta,以及 全域裝飾器,都將由此程式庫組成並以簡單元件的形式返回給您。這樣,在您的單元測試中,您只需選擇要渲染的 story,所有必要的設定都將為您完成。這是實現測試編寫和 Storybook story 編寫之間更好共享性和維護性的關鍵。

安裝

此程式庫應作為您專案的 devDependencies 之一安裝

透過 npm

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

或透過 yarn

yarn add --dev @storybook/testing-react

設定

Storybook 6 和元件 Story 格式

此程式庫要求您使用 Storybook 6 版本、元件 Story 格式 (CSF)提升的 CSF 註解,這是自 Storybook 6 以來推薦的 story 編寫方式。

基本上,如果您使用 Storybook 6 且您的 story 看起來與此類似,那麼您就準備就緒了!

// 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,
};

全域配置

這是一個可選步驟。如果您沒有 全域裝飾器,則無需執行此操作。但是,如果您有,這是您的全域裝飾器必須套用的步驟。

如果您有全域裝飾器/參數/等,並且希望它們在測試時套用至您的 story,您首先需要設定它。您可以透過新增到或建立 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 將處理您指定的元件中的所有 story,在它們中組合 args/裝飾器,並傳回一個包含組合 story 的物件。

如果您使用組合的 story (例如 PrimaryButton),元件將使用 story 中傳遞的 args 進行渲染。但是,您可以自由地在元件之上傳遞任何 props,這些 props 將覆蓋 story 的 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

如果您希望將其套用於單個 story 而不是所有 story,則可以使用 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: {...})

重複使用 story 屬性

composeStoriescomposeStory 傳回的元件不僅可以渲染為 React 元件,而且還具有來自 story、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,其中 story 也可以是一個物件。@storybook/testing-react 中支援此功能,但您必須符合其中一個要求

1 - 您的 storyrender 方法 2 - 或您的 metarender 方法 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 的新函數,您可以在其中編寫 story 的自動互動。

@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!');
});

批次測試檔案中的所有 story

您也可以使用 test.eachcomposeStories 的組合來執行自動化測試,而不是手動指定逐個測試。以下是從檔案中對所有 story 執行快照測試的範例

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,並提供自動完成功能以輕鬆偵測元件的所有 story

component autocompletion

它還提供元件的 props,就像您通常在測試中直接使用它們一樣

props autocompletion

僅在 tsconfig.json 檔案中將 strictstrictBindApplyCall 模式設定為 true 的專案中,才可能進行類型推斷。您還需要 4.0.0 以上版本的 TypeScript。如果您沒有適當的類型推斷,這可能是原因。

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

免責聲明

為了自動接收類型,您的 story 必須是類型化的。請參閱範例

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
標籤