文件
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 轉換為可渲染的元素,以便在 JSDOM 的 Node 測試中重複使用。它還允許您將專案中啟用的其他 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 以應用特定的 `decorators` 或 `parameters`。

為了避免這種情況,您可以透過擴展 `composeStory` 或 `composeStories` 函數來提供特定於測試的設定,從而覆寫全域設定。例如

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,包括您定義的任何 `args` 或 `decorators`。例如

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 2 和 Angular 等其他框架提供了社群主導的附加元件。但是,這些附加元件仍然缺乏對最新穩定版 Storybook 的支援。如果您有興趣提供幫助,我們建議您使用預設的溝通管道 (GitHub 和 Discord 伺服器) 聯繫維護者。

args 未傳遞到測試

由 `composeStories` 或 `composeStory` 返回的組件不僅可以渲染為 React 組件,還具有來自 story、meta 和全域設定的組合屬性。這表示如果您想存取 args 或參數,例如,您可以這樣做

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