說明
支持我們

使用 Preact 測試函式庫進行測試

Preact 測試函式庫preact/test-utils 的輕量級包裝器。它提供一組查詢方法,用於存取已渲染的 DOM,方式類似於使用者在頁面上尋找元素的方式。這種方法可讓您撰寫不依賴於實作細節的測試。因此,當被測試的元件進行重構時,這會讓測試更容易維護且更具彈性。

Enzyme 不同,Preact 測試函式庫必須在 DOM 環境中呼叫。



安裝

透過以下命令安裝 testing-library Preact 介面卡

npm install --save-dev @testing-library/preact

注意:此函式庫仰賴 DOM 環境存在。如果您使用 Jest,它已包含在內並預設啟用。如果您使用其他測試執行器,例如 MochaJasmine,您可以透過安裝 jsdom 來將 DOM 環境新增至節點。

用法

假設我們有一個 Counter 元件,它會顯示初始值,並有一個按鈕可以更新它

import { h } from 'preact';
import { useState } from 'preact/hooks';

export function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  const increment = () => setCount(count + 1);

  return (
    <div>
      Current value: {count}
      <button onClick={increment}>Increment</button>
    </div>
  );
}

我們要驗證我們的 Counter 是否顯示初始計數,以及按一下按鈕是否會增加計數。使用您選擇的測試執行器,例如 JestMocha,我們可以寫下這兩個場景

import { expect } from 'expect';
import { h } from 'preact';
import { render, fireEvent, screen, waitFor } from '@testing-library/preact';

import Counter from '../src/Counter';

describe('Counter', () => {
  test('should display initial count', () => {
    const { container } = render(<Counter initialCount={5}/>);
    expect(container.textContent).toMatch('Current value: 5');
  });

  test('should increment after "Increment" button is clicked', async () => {
    render(<Counter initialCount={5}/>);

    fireEvent.click(screen.getByText('Increment'));
    await waitFor(() => {
      // .toBeInTheDocument() is an assertion that comes from jest-dom.
      // Otherwise you could use .toBeDefined().
      expect(screen.getByText("Current value: 6")).toBeInTheDocument();
    });
  });
});

您可能已注意到那裡的 waitFor() 呼叫。我們需要它來確保 Preact 有足夠的時間呈現在 DOM 中,並清除所有待處理的效果。

test('should increment counter", async () => {
  render(<Counter initialCount={5}/>);

  fireEvent.click(screen.getByText('Increment'));
  // WRONG: Preact likely won't have finished rendering here
  expect(screen.getByText("Current value: 6")).toBeInTheDocument();
});

在幕後,waitFor 會重複呼叫傳遞的回呼函式,直到它不再擲回錯誤或逾時(預設:1000 毫秒)。在上述範例中,我們知道更新已完成,當計數器增加且新值呈現在 DOM 中時。

我們也可以使用查詢的「findBy」版本,而不是「getBy」來以非同步優先的方式撰寫測試。非同步查詢會在幕後使用 waitFor 進行重試,並傳回 Promise,因此您需要等待它們。

test('should increment counter", async () => {
  render(<Counter initialCount={5}/>);

  fireEvent.click(screen.getByText('Increment'));

  await screen.findByText('Current value: 6'); // waits for changed element

  expect(screen.getByText("Current value: 6")).toBeInTheDocument(); // passes
});

尋找元素

在完整的 DOM 環境中,我們可以直接驗證我們的 DOM 節點。測試通常會檢查屬性是否存在,例如輸入值或元素是否出現/消失。為此,我們需要能夠在 DOM 中找到元素。

使用內容

測試庫的理念是「您的測試越像您的軟體使用方式,它們就能給您越多的信心」。

與頁面互動的建議方式是透過文字內容,就像使用者所做的那樣來尋找元素。

您可以在 「我應該使用哪個查詢」 頁面中找到選擇正確查詢的指南。最簡單的查詢是 getByText,它會查看元素的 textContent。還有標籤文字、佔位符、標題屬性等的查詢。getByRole 查詢是最強大的,因為它抽象化了 DOM,並允許您在無障礙樹中找到元素,這是螢幕閱讀器讀取您的頁面的方式。結合 roleaccessible name 可以用單一查詢涵蓋許多常見的 DOM 遍歷。

import { render, fireEvent, screen } from '@testing-library/preact';

test('should be able to sign in', async () => {
  render(<MyLoginForm />);

  // Locate the input using textbox role and the accessible name,
  // which is stable no matter if you use a label element, aria-label, or
  // aria-labelledby relationship
  const field = await screen.findByRole('textbox', { name: 'Sign In' });

  // type in the field
  fireEvent.change(field, { value: 'user123' });
})

有時,當內容經常變更,或者您使用將文字翻譯成不同語言的國際化架構時,直接使用文字內容會產生摩擦。您可以透過將文字視為您擷取快照的資料來解決這個問題,這使得更新變得容易,但將真實來源保留在測試之外。

test('should be able to sign in', async () => {
  render(<MyLoginForm />);

  // What if we render the app in another language, or change the text? Test fails.
  const field = await screen.findByRole('textbox', { name: 'Sign In' });
  fireEvent.change(field, { value: 'user123' });
})

即使您不使用翻譯架構,您也可以將字串保存在一個獨立的檔案中,並使用與以下範例相同的策略

test('should be able to sign in', async () => {
  render(<MyLoginForm />);

  // We can use our translation function directly in the test
  const label = translate('signinpage.label', 'en-US');
  // Snapshot the result so we know what's going on
  expect(label).toMatchInlineSnapshot(`Sign In`);

  const field = await screen.findByRole('textbox', { name: label });
  fireEvent.change(field, { value: 'user123' });
})

使用測試 ID

測試 ID 是新增至 DOM 元素的資料屬性,以協助在選擇內容模稜兩可或難以預測,或與 DOM 結構等實作細節脫鉤時的情況。當其他尋找元素的方法都沒有意義時,可以使用它們。

function Foo({ onClick }) {
  return (
    <button onClick={onClick} data-testid="foo">
      click here
    </button>
  );
}

// Only works if the text stays the same
fireEvent.click(screen.getByText('click here'));

// Works if we change the text
fireEvent.click(screen.getByTestId('foo'));

除錯測試

若要除錯目前的 DOM 狀態,可以使用 debug() 函式列印 DOM 的美化版本。

const { debug } = render(<App />);

// Prints out a prettified version of the DOM
debug();

提供自訂 Context 提供者

很多時候,您會遇到依賴共用 context 狀態的元件。常見的提供者通常從路由器、狀態到有時是主題,以及其他對您的特定應用程式而言是全域的提供者。這可能會讓每次重複設定每個測試案例變得繁瑣,因此我們建議透過包裝 @testing-library/preact 中的函式來建立自訂 render 函式。

// helpers.js
import { render as originalRender } from '@testing-library/preact';
import { createMemoryHistory } from 'history';
import { FooContext } from './foo';

const history = createMemoryHistory();

export function render(vnode) {
  return originalRender(
    <FooContext.Provider value="foo">
      <Router history={history}>
        {vnode}
      </Router>
    </FooContext.Provider>
  );
}

// Usage like usual. Look ma, no providers!
render(<MyComponent />)

測試 Preact Hooks

使用 @testing-library/preact,我們也可以測試我們的 hooks 的實作!想像一下,我們想要為多個元件重複使用計數器功能(我知道我們很喜歡計數器!)並已將其萃取到一個 hook 中。現在我們想要測試它。

import { useState, useCallback } from 'preact/hooks';

const useCounter = () => {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => setCount(c => c + 1), []);
  return { count, increment };
}

與之前一樣,背後的方法類似:我們想要驗證我們可以遞增我們的計數器。因此,我們需要以某種方式呼叫我們的 hook。這可以使用 renderHook() 函式來完成,該函式會自動在內部建立一個周圍元件。此函式會在 result.current 下傳回目前的 hook 傳回值,我們可以使用它來進行驗證

import { renderHook, act } from '@testing-library/preact';
import useCounter from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());

  // Initially the counter should be 0
  expect(result.current.count).toBe(0);

  // Let's update the counter by calling a hook callback
  act(() => {
    result.current.increment();
  });

  // Check that the hook return value reflects the new state.
  expect(result.current.count).toBe(1);
});

有關 @testing-library/preact 的更多資訊,請查看 https://github.com/testing-library/preact-testing-library