使用 Preact 測試函式庫進行測試
Preact 測試函式庫是 preact/test-utils
的輕量級包裝器。它提供一組查詢方法,用於存取已渲染的 DOM,方式類似於使用者在頁面上尋找元素的方式。這種方法可讓您撰寫不依賴於實作細節的測試。因此,當被測試的元件進行重構時,這會讓測試更容易維護且更具彈性。
與 Enzyme 不同,Preact 測試函式庫必須在 DOM 環境中呼叫。
安裝
透過以下命令安裝 testing-library Preact 介面卡
npm install --save-dev @testing-library/preact
注意:此函式庫仰賴 DOM 環境存在。如果您使用 Jest,它已包含在內並預設啟用。如果您使用其他測試執行器,例如 Mocha 或 Jasmine,您可以透過安裝 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 是否顯示初始計數,以及按一下按鈕是否會增加計數。使用您選擇的測試執行器,例如 Jest 或 Mocha,我們可以寫下這兩個場景
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,並允許您在無障礙樹中找到元素,這是螢幕閱讀器讀取您的頁面的方式。結合 role
和 accessible 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 。