單元測試中的 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) 應用於測試中,使您能夠在選擇的測試環境 (例如,Jest、Vitest) 中重複使用 stories,確保您的測試始終與 stories 同步,而無需重寫它們。這就是我們在 Storybook 中所說的可攜式 stories。
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
` 函數來提供特定於測試的設定,從而覆寫全域設定。例如
// 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 的正確資訊。例如
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
`。例如
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 或參數,例如,您可以這樣做
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 測試