回到部落格

如何測試組件互動

學習如何模擬使用者行為並執行功能檢查

loading
Varun Vachhar
@winkerVSbecks
上次更新

你撥動開關,燈卻沒有亮。可能是燈泡燒壞了,也可能是電線故障。開關和燈泡透過牆壁內的電線連接在一起。

應用程式也是如此。表面上是使用者看到和互動的 UI。在底層,UI 連接著各種線路,以促進資料和事件的流動。

隨著你建構更複雜的 UI(例如頁面),組件的職責不僅僅是渲染 UI。它們還會獲取資料和管理狀態。本文將逐步介紹互動式組件的測試。你將學習如何使用電腦來模擬和驗證使用者互動。

那個組件真的能正常運作嗎?

組件的主要任務是根據一組 props 渲染 UI 的一部分。更複雜的組件還會追蹤應用程式狀態,並將行為向下傳遞到組件樹中。

例如,組件將從初始狀態開始。當使用者在輸入欄位中輸入內容或點擊按鈕時,它會在應用程式內觸發一個事件。組件會響應此事件來更新狀態。然後,這些狀態變更會更新渲染的 UI。這就是互動的完整週期。

考慮一下我在先前的文章中介紹的 Taskbox 應用程式。在 InboxScreen 上,使用者可以點擊星號圖示來釘選任務。或點擊核取方塊來封存它。視覺化測試確保組件在所有這些狀態下看起來都正確。我們還需要確保 UI 能夠正確響應這些互動。

以下是互動測試工作流程的外觀

  1. 📝 設定: 隔離組件並為初始狀態提供適當的 props。
  2. 🤖 動作: 渲染組件並模擬互動。
  3. 執行斷言 以驗證狀態是否已正確更新。

Taskbox 應用程式是使用 Create React App 引導啟動的,它預先配置了 Jest。這就是我們將用來編寫和執行測試的工具。

測試組件的功能,而不是其運作方式

就像單元測試一樣,我們希望避免測試組件的內部運作方式。這會使測試變得脆弱,因為無論輸出是否變更,只要你重構程式碼,它就會破壞測試。這反過來會減慢你的速度。

這就是為什麼 Adobe、Twilio、Gatsby 和更多團隊使用 Testing-Library 的原因。它允許你評估渲染的輸出。它的運作方式是在虛擬瀏覽器 (JSDOM) 中掛載組件,並提供複製使用者互動的實用程式。

我們可以編寫模擬真實世界使用情況的測試,而不是存取組件的內部狀態和方法。從使用者的角度編寫測試,讓我們更有信心我們的程式碼能夠正常運作。

讓我們深入研究一些程式碼,看看這個過程是如何運作的。我們這次的起點是 composition-testing 分支。

重複使用 stories 作為互動測試案例

我們先編寫一個測試案例。先前,我們在 InboxScreen.stories.js 檔案中編目了 InboxScreen 組件的所有用例。這讓我們能夠在開發期間進行外觀檢查,並透過視覺化測試來捕捉回歸錯誤。這些 stories 現在也將為我們的互動測試提供支援。

// InboxScreen.stories.js
import React from 'react';
import { rest } from 'msw';
import { InboxScreen } from './InboxScreen';
import { Default as TaskListDefault } from './components/TaskList.stories';
 
export default {
 component: InboxScreen,
 title: 'InboxScreen',
};
 
const Template = (args) => <InboxScreen {...args} />;
 
export const Default = Template.bind({});
Default.parameters = {
 msw: [
   rest.get('/tasks', (req, res, ctx) => {
     return res(ctx.json(TaskListDefault.args));
   }),
 ],
};
 
export const Error = Template.bind({});
Error.args = {
 error: 'Something',
};
Error.parameters = {
 msw: [
   rest.get('/tasks', (req, res, ctx) => {
     return res(ctx.json([]));
   }),
 ],
};

Stories 是以基於標準 JavaScript 模組的可移植格式編寫的。你可以將它們與任何基於 JavaScript 的測試函式庫(Jest、Testing Lib、Playwright)重複使用。這讓你無需為套件中的每個測試工具設定和維護測試案例。例如,Adobe Spectrum 設計系統團隊使用這種模式來測試互動,適用於他們的選單和對話方塊組件。

當你將測試案例編寫為 stories 時,任何形式的斷言都可以疊加在頂部。讓我們試試看。建立 InboxScreen.test.js 檔案並編寫第一個測試。與上面的範例一樣,我們正在將一個 story 匯入到這個測試中,並使用 Testing-Library 中的 render 函數來掛載它。

it 區塊描述了我們的測試。我們首先渲染組件,等待它獲取資料,找到特定的任務,然後點擊釘選按鈕。斷言檢查釘選狀態是否已更新。最後,afterEach 區塊會清理在測試期間掛載的 React 樹。

// InboxScreen.test.js
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render, waitFor, cleanup } from '@testing-library/react';
import * as stories from './InboxScreen.stories';
 
describe('InboxScreen', () => {
 afterEach(() => {
   cleanup();
 });
 
 const { Default } = stories;
 
 it('should pin a task', async () => {
   const { queryByText, getByRole } = render(<Default />);
 
   await waitFor(() => {
     expect(queryByText('You have no tasks')).not.toBeInTheDocument();
   });
 
   const getTask = () => getByRole('listitem', { name: 'Export logo' });
 
   const pinButton = within(getTask()).getByRole('button', { name: 'pin' });
 
   fireEvent.click(pinButton);
 
   const unpinButton = within(getTask()).getByRole('button', {
     name: 'unpin',
   });
 
   expect(unpinButton).toBeInTheDocument();
 });
});

執行 yarn test 以啟動 Jest。你會注意到測試失敗了。

InboxScreen 從後端獲取資料。在先前的文章中,我們設定了 Storybook MSW 擴充套件 來模擬這個 API 請求。但是,這在 Jest 中不可用。我們需要一種方法來引入這個和其他組件依賴項。

組件配置開始

複雜的組件依賴於外部依賴項,例如主題提供器和上下文,以共享全域資料。Storybook 使用裝飾器來包裝 story 並提供此類功能。為了匯入 stories 以及它們的所有配置,我們將使用 @storybook/testing-react 函式庫。

這通常是一個兩步驟的過程。首先,我們需要註冊所有全域裝飾器。在我們的案例中,我們有兩個:一個提供 Chakra UI 主題的裝飾器和一個用於 MSW 擴充套件的裝飾器。我們先前.storybook/preview 中配置了這些。

Jest 提供了一個全域設定檔 setupTests.js,當專案引導啟動時,由 CRA 自動產生。更新該檔案以註冊 Storybook 的全域配置。

// setupTests.js
import '@testing-library/jest-dom';
 
import { setGlobalConfig } from '@storybook/testing-react';
import * as globalStorybookConfig from '../.storybook/preview';
 
setGlobalConfig(globalStorybookConfig);

接下來,更新測試以使用 @storybook/testing-react 中的 composeStories 實用程式。它傳回 stories 的 1:1 對應,所有裝飾器都已應用於它們。瞧,我們的測試通過了!

// InboxScreen.test.js
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render, waitFor, cleanup } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import { getWorker } from 'msw-storybook-addon';
import * as stories from './InboxScreen.stories';
 
describe('InboxScreen', () => {
 afterEach(() => {
   cleanup();
 });
 
 // Clean up after all tests are done, preventing this
 // interception layer from affecting irrelevant tests
 afterAll(() => getWorker().close());
 
 const { Default } = composeStories(stories);
 
 it('should pin a task', async () => {
   const { queryByText, getByRole } = render(<Default />);
 
   await waitFor(() => {
     expect(queryByText('You have no tasks')).not.toBeInTheDocument();
   });
 
   const getTask = () => getByRole('listitem', { name: 'Export logo' });
 
   const pinButton = within(getTask()).getByRole('button', { name: 'pin' });
 
   fireEvent.click(pinButton);
 
   const unpinButton = within(getTask()).getByRole('button', {
     name: 'unpin',
   });
 
   expect(unpinButton).toBeInTheDocument();
 });
});

我們已成功編寫了一個測試,該測試載入了一個 story 並使用 Testing Library 渲染它。然後,它應用模擬的使用者行為,並檢查組件狀態是否已準確更新。

使用相同的模式,我們還可以為封存和編輯場景新增測試。

 it('should archive a task', async () => {
   const { queryByText, getByRole } = render(<Default />);
 
   await waitFor(() => {
     expect(queryByText('You have no tasks')).not.toBeInTheDocument();
   });
 
   const task = getByRole('listitem', { name: 'QA dropdown' });
   const archiveCheckbox = within(task).getByRole('checkbox');
   expect(archiveCheckbox.checked).toBe(false);
 
   fireEvent.click(archiveCheckbox);
   expect(archiveCheckbox.checked).toBe(true);
 });
 
 it('should edit a task', async () => {
   const { queryByText, getByRole } = render(<Default />);
 
   await waitFor(() => {
     expect(queryByText('You have no tasks')).not.toBeInTheDocument();
   });
 
   const task = getByRole('listitem', {
     name: 'Fix bug in input error state',
   });
   const taskInput = within(task).getByRole('textbox');
 
   const updatedTaskName = 'Fix bug in the textarea error state';
 
   fireEvent.change(taskInput, {
     target: { value: 'Fix bug in the textarea error state' },
   });
   expect(taskInput.value).toBe(updatedTaskName);
 });

總之,設定程式碼位於 stories 檔案中,而動作和斷言則位於測試檔案中。透過 Testing Library,我們以使用者會使用的方式與 UI 互動。未來,如果組件實作發生變更,則只有在輸出或行為被修改時,測試才會失敗。

Stories 是所有類型測試的起點

組件不是靜態的。使用者可以與 UI 互動並觸發狀態更新。為了驗證這些功能品質,你需要編寫模擬使用者行為的測試。互動測試檢查組件之間的連接,即事件和資料正在流動。以及底層邏輯是否正確。

將測試案例編寫為 stories 意味著你只需要進行一次棘手的設定:隔離組件、模擬它們的依賴項並捕捉它們的用例。然後,所有這些設定都可以匯入到其他測試框架中,從而節省你的時間和精力。

更重要的是,stories 的可移植性也簡化了無障礙測試。當你確保每個使用者都能使用你的 UI 時,你會影響企業財務並滿足法律要求。這是一個雙贏的局面。

但是無障礙功能似乎需要做很多工作。下一篇文章將分解如何在開發和品質保證期間自動化無障礙檢查。這允許你儘早發現違規行為,最終節省你的時間和精力。

加入 Storybook 電子郵件列表

獲取最新的新聞、更新和發佈

6,730位開發人員和持續增加中

我們正在招募!

加入 Storybook 和 Chromatic 背後的團隊。建立被數十萬開發人員用於生產環境的工具。優先考慮遠端工作。

查看職位

熱門文章

使用 Storybook 進行無障礙測試

透過整合工具實現快速回饋
loading
Varun Vachhar

互動測試搶先看

使用 Storybook 的 play 函數測試連接的組件
loading
Dominic Nguyen

測試複合組件

防止小變更變成重大回歸
loading
Varun Vachhar
加入社群
6,730位開發人員和持續增加中
為何選擇為何選擇 Storybook組件驅動的 UI
文件指南教學課程更新日誌遙測技術
社群擴充套件參與貢獻部落格
範例展示探索專案組件詞彙表
開源軟體
Storybook - Storybook 繁體中文

特別感謝 Netlify 以及 CircleCI