文件
Storybook 文件

單元測試中的 Stories

團隊使用不同的工具測試各種 UI 特性。每個工具都需要您重複複製相同的元件狀態。這是一個維護上的難題。理想情況下,您會以類似的方式設定測試,並在不同工具之間重複使用。

Storybook 讓您可以隔離元件,並在 *.stories.js|ts 檔案中擷取其使用案例。Stories 是標準的 JavaScript 模組,可與整個 JavaScript 生態系統跨相容。

Stories 是 UI 測試的實用起點。將 stories 導入 Jest、Testing Library、Vitest 和 Playwright 等工具,以節省時間和維護工作。

使用 Testing Library 撰寫測試

Testing Library 是一套用於瀏覽器型元件測試的輔助程式庫。使用元件 Story 格式,您的 stories 可在 Testing Library 中重複使用。每個具名的匯出 (story) 都可在您的測試設定中呈現。例如,如果您正在處理登入元件,並想要測試無效認證情境,以下說明如何撰寫測試

Storybook 提供 composeStories 工具,可協助將測試檔案中的 stories 轉換為可呈現的元素,這些元素可在您的 Node 測試中使用 JSDOM 重複使用。它也讓您可以將專案中啟用的其他 Storybook 功能 (例如,裝飾器args) 應用到您的測試中,讓您可以在選擇的測試環境 (例如,JestVitest) 中重複使用 stories,確保您的測試始終與 stories 同步,而無需重寫它們。這就是我們在 Storybook 中所指的可攜式 stories。

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();
});

必須設定測試環境以使用可攜式 stories,以確保您的 stories 與您 Storybook 設定的所有方面 (例如,裝飾器) 組成。

測試執行後,它會載入 story 並呈現它。Testing Library 接著會模擬使用者的行為,並檢查元件狀態是否已更新。

覆寫 story 屬性

根據預設,setProjectAnnotations 函式會將您在 Storybook 執行個體中定義的任何全域設定 (即,preview.js|ts 檔案中的參數、裝飾器) 注入到您現有的測試中。儘管如此,這可能會對不打算使用這些全域設定的測試造成無法預見的副作用。例如,您可能想要始終在特定地區設定中測試 story (透過 globalTypes) 或設定 story 以套用特定的 decoratorsparameters

為避免這種情況,您可以透過擴充 composeStorycomposeStories 函式來提供測試特定的設定,以覆寫全域設定。例如

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

在單一 story 上執行測試

您可以使用 composeStory 函式,讓您的測試在單一 story 上執行。但是,如果您依賴此方法,建議您將 story 中繼資料 (即,預設匯出) 提供給 composeStory 函式。這可確保您的測試能夠準確判斷 story 的正確資訊。例如

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();
});

將 stories 合併到單一測試中

如果您打算在單一測試中測試多個 stories,請使用 composeStories 函式。它會處理您指定的所有元件 story,包括您已定義的任何 argsdecorators。例如

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();
});

疑難排解

在其他框架中執行測試

Storybook 為其他框架(如 Vue 2Angular)提供了社群主導的附加元件。然而,這些附加元件仍然缺乏對最新穩定版 Storybook 的支援。如果您有興趣提供協助,我們建議您使用預設的溝通管道(GitHub 和 Discord 伺服器)聯絡維護者。

參數未傳遞至測試中

composeStoriescomposeStory 返回的元件不僅可以渲染為 React 元件,還具有來自 story、meta 和全域配置的組合屬性。這表示如果您想要存取參數或引數,例如,您可以這麼做

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 測試