
Storybook 中的元件測試
UI 測試的未來

在過去十年中,網頁 UI 技術突飛猛進。儘管如此,在 2024 年構建/維護生產 UI 仍然比以往任何時候都更加困難。
在 Storybook,我們與全球數千個頂尖 UI 團隊合作,包括 Microsoft、Supabase 和 JPMorganChase 等公司。無論團隊規模大小,或最終結果多麼精良,我們都看到他們在管理複雜前端開發方面遇到類似的困難。
許多團隊希望他們的 UI 具有測試覆蓋率以捕捉回歸錯誤,但他們無法承擔維護大型端對端測試套件的成本(我們將在下面更詳細地探討)。同時,他們通常有數千個單元測試,但這些測試無法給他們帶來太多 UI 信心,因為它們是在 Node 中使用模擬瀏覽器環境運行的。
在一次又一次看到相同的模式後,我們押注元件測試將成為 UI 測試的未來。
元件測試會在瀏覽器中渲染 UI 元件,使其獨立於應用程式的其餘部分。它還可以與元件互動並進行斷言。
元件測試在 UI 測試中找到了最佳平衡點,它提供了端對端風格的瀏覽器保真度,同時兼具單元測試的速度、可靠性和精簡性。
元件測試並非要取代端對端或單元測試,而是完美的補充。請繼續閱讀以了解更多關於元件測試的資訊、它如何融入更廣泛的測試領域,以及為什麼我們認為它非常適合您的大部分 UI 測試。
什麼是元件測試?
如果您在過去十年中一直在 JavaScript 生態系統中構建,您可能已經看過像這樣的單元測試(由 Testing Library 提供)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Fetch from './fetch';
it('loads and displays greeting', async () => {
// ARRANGE
render(<Fetch url="/greeting" />);
// ACT
await userEvent.click(
screen.getByText('Load Greeting')
);
await screen.findByRole('heading');
// ASSERT
expect(screen.getByRole('heading'))
.toHaveTextContent('hello there');
expect(screen.getByRole('button')).toBeDisabled();
});
此測試渲染一個名為 `Fetch` 的元件,透過 DOM 與其互動,然後根據該互動斷言 DOM 的變化。
它看起來有點像端對端 (E2E) 測試,因為它模擬使用者與某些應用程式 UI 互動。元件可以小到一個按鈕,也可以大到整個應用程式頁面,而且您在樹狀結構中越往上走,它就越像 E2E。
但它不是 E2E,因為它是在隔離於應用程式其餘部分的情況下測試單個元件。這種差異既是元件測試的優勢也是劣勢,這取決於您想要測試什麼,我們將在下面看到。
此外,這個範例 (Jest + Testing Library) 在 Node 上運行,並基於像 JSDom 這樣的 DOM 模擬層。因為它僅在瀏覽器的模擬中運行,所以在 JSDom 中通過的測試在真實世界場景中可能會失敗,反之亦然。
像 Storybook、Vitest、Playwright、Cypress、Webdriver 和 Nightwatch 這樣的工具也會渲染和測試元件,但它們是在實際的瀏覽器中進行的。這些測試是我們定義為元件測試的。
因此,元件測試
- 在瀏覽器中渲染元件以實現高保真度
- 模擬使用者與實際 UI 互動,就像 E2E 測試一樣
- 僅測試 UI 的一個單元(例如,單個元件),並且可以深入實作中模擬事物或操縱數據,就像單元測試一樣
元件測試:完美的補充
正如我們在上面看到的,元件測試同時具有單元測試和 E2E 測試的元素。但是,為什麼元件測試有用?您應該在何時使用它們?
讓我們從一個簡單的主張開始
E2E 測試是保真度最高的測試,因為它們準確地測試了使用者在使用您的應用程式時將看到的內容。
在沒有任何其他考慮因素的情況下,如果您可以使用 E2E 測試來測試 UI 的特定功能,那麼您應該這樣做。E2E 測試讓您最有信心一切都能正常協同工作。
但是,如果 E2E 如此出色(確實如此!),為什麼許多團隊卻很少使用它們呢?
問題在於「其他考量因素」。儘管 E2E 測試取得了許多進展,但仍然存在實際限制,使得對 UI 的每個方面進行 E2E 測試具有挑戰性。
挑戰包括
- 較慢的測試運行速度,容易出現不穩定
- 許多「難以觸及」的狀態
- 設定和測試後端的額外開銷
- 黑箱環境只能從外部操作
所有這些挑戰都可以透過元件測試來解決,但代價是無法測試整個系統。
這使得這兩種技術成為完美的互補
- E2E 測試可以涵蓋應用程式中的少量順利路徑
- 元件測試可以涵蓋更廣泛的其他重要 UI 狀態
而這正是我們認為應該如何測試 UI 的方式。

Mealdrop 範例應用程式
為了使這個命題更具體,讓我們考慮 Mealdrop,這是一個實現食品外送服務的範例專案

E2E 測試
此應用程式的順利路徑從首頁開始,導航到餐廳,將商品新增到購物車,然後結帳。
我們已在 Playwright 中實作此流程,並使用 Chromatic 對每個步驟進行視覺化測試。該測試會導航到每個頁面,並在每個狀態下拍攝 UI 的視覺快照,以確保頁面正確渲染。它還在過程中對 DOM 進行了一些關鍵的斷言。為了簡潔起見,測試已縮寫如下;完整的測試可在 Mealdrop 儲存庫中找到。
import { test, expect } from '@playwright/test';
test('should complete the full user journey from home to success page', async ({ page }) => {
await page.goto('http://localhost:3000');
// Navigate to Restaurants page
await page.getByText('View all restaurants').click();
// Select "Burgers" category
await page.getByText('Burgers').click();
// Select the first restaurant from the list
const restaurantCards = await page.getAllByTestId('restaurant-card');
await restaurantCards.first().click();
// Add Cheeseburger to the cart
const foodItem = await page.getByText(/Cheeseburger/i);
await foodItem.click();
// Go to "Checkout" page
await page.getByText(/checkout/i).click();
// Fill in order details...
// Complete the order
await page.getByRole('button', { name: 'Complete order' }).click();
await expect(page.locator('h1')).toContainText('Order confirmed!');
});
這個單一測試涵蓋了單一流程中的各種狀態,模擬了使用者在應用程式中的實際體驗。在配備 16G RAM 的 2021 MacBook M1 Pro 上運行需要 5-6 秒。
但是,還有許多狀態未被涵蓋,例如載入和錯誤狀態、表單驗證檢查等等。為了涵蓋它們,我們可以新增更多 E2E 測試,這些測試會採用應用程式中的不同路徑並有意觸發我們遺失的狀態。但是,根據我們上面的論點,我們選擇使用元件測試來涵蓋這些狀態。
元件測試
為了涵蓋遺失的狀態,我們使用 Storybook 進行元件測試。每個 story 都是一個小的程式碼片段,用於將元件配置為關鍵 UI 狀態。讓我們考慮 Mealdrop 的 `RestaurantDetailPage` 元件的幾個 story。
最簡單的 story,`Success`,幾乎不像一個測試。它透過使用 Mock Service Worker 模擬 `RestaurantDetailPage` 元件使用的資料並驗證元件是否成功渲染來執行冒煙測試
// RestaurantDetailPage.stories.tsx
import { Meta, StoryObj } from '@storybook/react';
import { http, HttpResponse } from 'msw';
import { expect } from '@storybook/test';
import { BASE_URL } from '../../api';
import { restaurants } from '../../stub/restaurants';
import { RestaurantDetailPage } from './RestaurantDetailPage';
const meta = {
component: RestaurantDetailPage,
// All stories render the component and a spot to render the modal
render: () => {
return (
<>
<RestaurantDetailPage />
<div id="modal" />
</>
);
},
} satisfies Meta<typeof RestaurantDetailPage>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Success = {
parameters: {
// Mock data dependency
msw: {
handlers: [
http.get(BASE_URL, () => HttpResponse.json(restaurants[0])),
],
},
},
} satisfies Story;

但是 story 也可以使用 `play` 函數與瀏覽器互動並斷言其內容。例如,`WithModalOpen` 點擊餐廳的其中一個菜單項目,並驗證結果彈出視窗是否存在於 DOM 中
// RestaurantDetailPage.stories.tsx
export const WithModalOpen = {
...Success,
play: async ({ canvas, userEvent }) => {
const item = await canvas.findByText(/Cheeseburger/i);
await userEvent.click(item);
await expect(canvas.getByTestId('modal')).toBeInTheDocument();
},
} satisfies Story;

最後,我們可以模擬網路請求以模擬存取錯誤,例如這個 404 `NotFound` story
// RestaurantDetailPage.stories.tsx
export const NotFound = {
parameters: {
msw: {
handlers: [
// Mock a 404 response
http.get(BASE_URL, () => HttpResponse.json(null, { status: 404 })),
],
},
},
play: async ({ canvas }) => {
const item = await canvas.findByText(/We can't find this page/i);
await expect(item).toBeInTheDocument();
},
} satisfies Story;

與將整個應用程式視為黑箱進行互動的 E2E 測試不同,元件測試可以自由地模擬或監視堆疊的任何層級,只要作者認為合適。
因此,可以達到任何 UI 狀態——這在 E2E 測試中可能非常具有挑戰性。Mealdrop 的 45 個元件中的大多數都基於如上所示的 story 實現了 100% 的測試覆蓋率。
此外,這些測試運行速度非常快。在配備 16G RAM 的 2021 MacBook M1 Pro 上,整個 89 個測試的套件在瀏覽器中運行需要 8-10 秒,這僅比運行上面的單個 E2E 測試所需的時間稍長。
立即試用
Storybook 8.2 支援元件測試。在新專案中試用
npx storybook@latest init
或升級現有專案
npx storybook@latest upgrade
對於本文中顯示的完整範例,請參閱 Mealdrop 儲存庫。若要瞭解更多資訊,請參閱 Storybook 的元件測試文件。
下一步是什麼?
端對端 (E2E) 測試功能強大,因為它們完全按照使用者看到的方式測試您的應用程式。但是,由於測試不穩定、執行速度和其他實際考量因素,編寫和維護大量 E2E 測試以涵蓋複雜應用程式中的所有關鍵 UI 狀態具有挑戰性。元件測試提供了完美的補充,這就是為什麼我們全力以赴將 Storybook 轉變為元件測試的強大工具。
在接下來的幾個月中,我們將在 Storybook 中發布各種 UI 測試改進。這些變更包括
- 使 Storybook 的 story 格式與其他測試工具達到同等水平。
- 與 Vitest 合作實現閃電般的快速測試執行。
- 能夠從 Storybook 的 UI 運行測試並查看結果。
- 在單次運行中組合多種類型的測試,包括功能測試、視覺化測試、a11y 測試等。
- 一種在開發和 CI 中無縫調試測試失敗的獨特方法。
如需我們正在考慮和積極開發的專案的概述,請查看 Storybook 的路線圖。
🆕 Storybook 中的元件測試
— Storybook (@storybookjs) 2024 年 8 月 29 日
我們押注元件測試將成為 UI 測試的未來。
繼續閱讀以了解更多關於元件測試的內容、原因和方法… pic.twitter.com/4nQcqB7AqB