使用 Enzyme 進行單元測試
Airbnb 的 Enzyme 是用於撰寫 React 元件測試的函式庫。它透過「轉接器」支援不同版本的 React 和類似 React 的函式庫。Preact 有一個由 Preact 團隊維護的轉接器。
Enzyme 支援使用工具(例如 Karma)在一般或無頭瀏覽器中執行的測試,或使用 jsdom(作為瀏覽器 API 的虛擬實作)在 Node 中執行的測試。
有關使用 Enzyme 的詳細介紹和 API 參考,請參閱 Enzyme 文件。本指南的其餘部分說明如何將 Enzyme 設定為與 Preact 搭配使用,以及 Enzyme 搭配 Preact 與 Enzyme 搭配 React 有何不同。
安裝
使用下列方式安裝 Enzyme 和 Preact 轉接器
npm install --save-dev enzyme enzyme-adapter-preact-pure
設定
在測試設定程式碼中,您需要設定 Enzyme 以使用 Preact 轉接器
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-preact-pure';
configure({ adapter: new Adapter() });
有關使用 Enzyme 搭配不同測試執行器的指南,請參閱 Enzyme 文件的 指南 部分。
範例
假設我們有一個簡單的 Counter
元件,它會顯示初始值,並有一個按鈕可以更新它
import { h } from 'preact';
import { useState } from 'preact/hooks';
export default function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(count + 1);
return (
<div>
Current value: {count}
<button onClick={increment}>Increment</button>
</div>
);
}
使用測試執行器(例如 mocha 或 Jest),您可以撰寫測試來檢查它是否按預期運作
import { expect } from 'chai';
import { h } from 'preact';
import { mount } from 'enzyme';
import Counter from '../src/Counter';
describe('Counter', () => {
it('should display initial count', () => {
const wrapper = mount(<Counter initialCount={5}/>);
expect(wrapper.text()).to.include('Current value: 5');
});
it('should increment after "Increment" button is clicked', () => {
const wrapper = mount(<Counter initialCount={5}/>);
wrapper.find('button').simulate('click');
expect(wrapper.text()).to.include('Current value: 6');
});
});
有關此專案和其它範例的可執行版本,請參閱 Preact 轉接器儲存庫中的 範例/ 目錄。
Enzyme 的運作方式
Enzyme 使用已設定的轉接器函式庫來呈現元件及其子元件。然後轉接器將輸出轉換為標準化的內部表示(「React 標準樹」)。接著,Enzyme 會將其包裝在一個物件中,該物件具有查詢輸出並觸發更新的方法。包裝器物件的 API 使用類似 CSS 的 選擇器 來定位輸出的部分。
完整、淺層和字串呈現
Enzyme 有三種呈現「模式」
import { mount, shallow, render } from 'enzyme';
// Render the full component tree:
const wrapper = mount(<MyComponent prop="value"/>);
// Render only `MyComponent`'s direct output (ie. "mock" child components
// to render only as placeholders):
const wrapper = shallow(<MyComponent prop="value"/>);
// Render the full component tree to an HTML string, and parse the result:
const wrapper = render(<MyComponent prop="value"/>);
mount
函式會以與在瀏覽器中呈現相同的方式呈現元件及其所有後代元件。shallow
函式僅呈現元件直接輸出的 DOM 節點。任何子元件都會以僅輸出其子元件的佔位符取代。這種模式的優點是,您可以在不依賴子元件的詳細資料,且不需要建構其所有相依項的情況下,為元件撰寫測試。
shallow
呈現模式在 Preact 轉接器中的內部運作方式與 React 不同。有關詳細資訊,請參閱下方的差異區段。render
函式(不要與 Preact 的render
函式混淆!)將元件呈現為 HTML 字串。這對於測試伺服器上呈現的輸出,或在不觸發任何效果的情況下呈現元件很有用。
使用 act
觸發狀態更新和效果
在前一個範例中,.simulate('click')
用於按一下按鈕。
Enzyme 知道呼叫 simulate
可能會變更元件的狀態或觸發效果,因此它會在 simulate
回傳之前立即套用任何狀態更新或效果。當使用 mount
或 shallow
初始呈現元件,以及使用 setProps
更新元件時,Enzyme 也會執行相同的動作。
然而,如果事件發生在 Enzyme 方法呼叫之外,例如直接呼叫事件處理常式(例如按鈕的 onClick
屬性),則 Enzyme 將不會察覺變更。在這種情況下,測試需要觸發狀態更新和效果的執行,然後要求 Enzyme 更新其對輸出的檢視。
- 若要同步執行狀態更新和效果,請使用
preact/test-utils
中的act
函式來包裝觸發更新的程式碼 - 若要更新 Enzyme 對已呈現輸出的檢視,請使用 wrapper 的
.update()
方法
例如,以下是測試計數器遞增的不同版本,修改為直接呼叫按鈕的 onClick
屬性,而不是透過 simulate
方法
import { act } from 'preact/test-utils';
it('should increment after "Increment" button is clicked', () => {
const wrapper = mount(<Counter initialCount={5}/>);
const onClick = wrapper.find('button').props().onClick;
act(() => {
// Invoke the button's click handler, but this time directly, instead of
// via an Enzyme API
onClick();
});
// Refresh Enzyme's view of the output
wrapper.update();
expect(wrapper.text()).to.include('Current value: 6');
});
與 React 中 Enzyme 的差異
一般來說,使用 Enzyme + React 編寫的測試可以輕鬆地與 Enzyme + Preact 一起使用,反之亦然。這避免了在需要將最初為 Preact 編寫的元件改為與 React 一起使用,或反之亦然時,必須重寫所有測試。
然而,此轉接器與 Enzyme 的 React 轉接器之間有一些行為差異需要注意
- 「淺層」呈現模式在底層運作方式不同。它與 React 一致,只會「一層深」地呈現元件,但與 React 不同的是,它會建立真正的 DOM 節點。它也會執行所有正常的生命週期掛勾和效果。
simulate
方法會傳送實際的 DOM 事件,而在 React 轉接器中,simulate
只會呼叫on<EventName>
屬性- 在 Preact 中,狀態更新(例如在呼叫
setState
之後)會批次處理並非同步套用。在 React 中,狀態更新可以立即套用或根據情況批次處理。為了讓撰寫測試更簡單,Preact 介面會在透過介面上的setProps
或simulate
呼叫觸發的初始渲染和更新後,清除狀態更新和效果。當狀態更新或效果由其他方式觸發時,您的測試程式碼可能需要使用preact/test-utils
套件中的act
手動觸發效果和狀態更新的清除。
如需進一步詳細資訊,請參閱 Preact 介面的 README。