元件測試實務

元件測試是開始示範實際測試程式碼的絕佳位置。元件測試比簡易的單元測試更龐大,較端對端測試更複雜,並且可以示範與 DOM 的互動方式。從更具體的角度來看,React 的用法能讓網頁開發人員更容易將網站或網頁應用程式視為元件的組成。

因此,無論元件有多複雜,測試個別元件都是開始考慮新或現有應用程式的好方法。

本頁將逐步說明如何測試具有複雜外部依附元件的小型元件。您很容易測試未與任何其他程式碼互動的元件,例如按下按鈕並確認數字增加。事實上,非常少的程式碼就如此。測試沒有互動的程式碼時,其價值可能很有限。

(並非完整的教學課程,而後續章節的「自動化測試」則會逐步引導您使用範例程式碼來測試實際網站,以做為教學課程使用)。不過,本頁仍會介紹幾個實用元件測試的範例。)

要測試的元件

我們會使用 Vitest 及其 JSDOM 環境來測試 React 元件。這有助於我們模擬瀏覽器時,在指令列使用 Node 快速執行測試。

名稱清單,每個名稱旁邊都有「Choose」按鈕。
這個小型 React 元件會顯示網路中的使用者清單。

這個名為 UserList 的 React 元件會從網路擷取使用者清單,並且讓您選取其中一位使用者。系統會透過 useEffect 中的 fetch 取得使用者清單,選取處理常式則由 Context 傳入。程式碼如下:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

這個範例不會示範 React 的最佳做法 (例如,在 useEffect 中使用 fetch),但程式碼集可能包含許多類似的情況。更進一步來說,這些情況在一開始可能會是很棘手的測試。在本課程之後的章節中,我們會詳細說明編寫可測試的程式碼。

以下是我們範例中正在測試的項目:

  • 檢查是否為了回應網路資料而建立正確的 DOM。
  • 確認點選使用者時會觸發回呼。

每個元件都是獨一無二的。為什麼測試這個功能很有趣?

  • 這項工具使用全域 fetch 從網路要求實際資料,測試中可能不穩定或速度緩慢。
  • 匯入了另一個 UserRow 類別,我們可能不想隱含測試。
  • 這會使用 Context,這並非測試中程式碼的特定部分,通常是由父項元件提供。

建立簡短測試以開始

我們可以快速測試這項元件的基本功能。明確來說,這個範例不太實用!不過,在名為 UserList.test.tsx 的對等檔案中設定樣板很有幫助。提醒您,Vitest 等測試執行器預設會執行結尾為 .test.js 或類似的檔案,包括 .tsx

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

這項測試會斷言元件轉譯時,會包含「Users」文字。「即使」元件會將 fetch 傳送至網路也會產生副作用,但此元件仍然有效。測試結束時,fetch 仍在進行中,沒有設定端點。測試結束時,我們可能無法確認顯示的任何使用者資訊,至少不會等待逾時時間。

模擬 fetch()

在測試中,模擬是指將實際函式或類別替換為控管項目。這在幾乎所有類型的測試中都很常見,但最簡易的單元測試除外。我們會在斷言和其他基本功能中進一步介紹。

您可以模擬測試 fetch(),使其快速完成並傳回您預期的資料,而不是「實際」或不明資料。fetch 具有「全域」性質,表示您不必在程式碼中執行 importrequire

在 Vitest 中,您可以透過 vi.fn() 傳回的特殊物件呼叫 vi.stubGlobal 來模擬全域,這會建構稍後可以修改的模擬。我們會在本課程的後續章節中詳細說明這些方法,但您可以在下列程式碼實際執行:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

這段程式碼會新增模擬畫面,說明網路擷取 Response 的「虛構」版本,並等待其顯示。未顯示文字時,只要將 queryByText 中的查詢變更為新名稱,即可檢查此內容,測試將會失敗。

本範例使用了 Vitest 內建的模擬輔助程式,但其他測試架構也有類似的模擬方法。Vitest 是獨一無二的,請務必在所有測試完成後呼叫 vi.unstubAllGlobals(),或是設定同等的全域選項。如果沒有「復原」我們的工作,fetch 模擬可能會影響其他測試,而且每個要求都會以奇怪的 JSON 檔案回應!

匯入模擬資料

您可能已經注意到,UserList 元件本身會匯入名為 UserRow 的元件。雖然我們尚未加入程式碼,但可以發現其中顯示了使用者名稱:先前的測試會檢查「Sam」,且並未直接在 UserList 內顯示,因此必須來自 UserRow

使用者名稱在元件中移動的流程圖。
UserListTest 並未取得 UserRow 的瀏覽權限。

不過,UserRow 本身可能是複雜的元件,可能會擷取更多使用者資料,或是含有與我們的測試無關的副作用。移除變化形式可讓測試更加實用,尤其是隨著要測試的元件變得更複雜,且與依附元件的交互作用更大。

幸好,即使測試並非直接使用,您依然可以使用 Vitest 模擬某些匯入作業。這樣一來,使用這些匯入的程式碼都會隨附簡單或已知版本:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

就像模擬全域 fetch 一樣,這是一項強大的工具,但如果程式碼包含許多依附元件,就可能變得難以維護。再次提醒您,最好的方式是編寫可測試的程式碼。

點按並提供背景資訊

React 和其他 如 Lit 等程式庫都有名為 Context 的概念。程式碼範例包含 UserContext,若選擇使用者,就會叫用方法。這通常視為「提示細查」的替代方案,也就是回呼會直接傳送至 UserList

我們撰寫的測試工具未提供 UserContext。如果在沒有回應的情況下將點擊動作新增到 React 測試,這種做法在最壞的情況下會讓測試異常終止,而在最佳情況下,如果已在其他地方提供預設執行個體,會導致某些行為讓我們無法控制 (類似上述的未知 UserRow):

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

而是在算繪元件時,提供自己的 Context。這個範例使用 vi.fn() (Vitest 模擬函式) 的執行個體,可在事後使用,以檢查呼叫是否已發出呼叫,以及用於呼叫哪些引數。在我們的範例中,這會與先前範例中模擬的 fetch 互動,而測試可確認傳遞的 ID 是否為「sam」:

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

這是一個簡單但強大的模式,可讓您從要測試的核心元件中移除不相關的依附元件。

摘要

這是快速簡單的範例,說明如何建構元件測試來測試難以測試的 React 元件,並著重於確保元件能正確與其依附元件互動 (全域的 fetch、匯入的子元件和 Context)。

隨堂測驗

測試 React 元件的方法有哪些?

使用簡易的依附元件模擬複雜的依附元件,以便進行測試
使用 Context 插入依附元件
部署全球
檢查數字是否遞增