關於前端React組件測試(jest,Enzyme),網上有大量的入門文章,可以看看,但如果你確實想了解前端自動化測試,個人更推薦看官方的文檔和一些比較官方的測試案列,這裏推薦兩個:
- enzyme官方文檔,涵蓋了各種說明和API;
- jest官方文檔,涵蓋了各種說明和API;
- antd基礎組件庫,每個組件都有較豐富的測試用例;
本篇文章適合對前端組件測試有一定概念的同學,本文將包含以下幾點:
- shallow, mount, render三種方法渲染的區別;
- 組件事件的模擬及事件回調的mock;
- 異步事件的模擬;
三種方法渲染的區別
組件測試最重要的前提,就是你需要知道怎麼實例化自己的組件,然後才能去判斷是否渲染正常,交互怎麼樣,功能是不是都OK,而這就和下面要說的渲染方法相關,瞭解一下三種方法的區別,有助於自己少掉坑,少摳腦殼,少掉頭髮,用一個關於antd Select組件的示例來說明。
function RenderTest() {
return (
<div className="render">
<Select>
<Option key="1" value="1">test1</Option>
<Option key="2" value="2">test2</Option>
<Option key="3" value="3">test3</Option>
</Select>
</div>
);
}
describe('base render test', () => {
it('component mounted right', () => {
const wrapper = mount(
<RenderTest />
);
console.log('***mount***',
'Select:', wrapper.find('Select').length,
' Option:', wrapper.find('Option').length,
' Div', wrapper.find('div').length,
' class', wrapper.find('.render').length);
});
});
在瀏覽器中,掛載這個RenderTest,渲染出來的DomTree是這樣的:
然後在組件測試中分別用了三種方法來渲染,得到是下面的結果:
- Shallow:與語義一樣,膚淺表面的,也是淺渲染, 大概就是組件長啥樣,就渲染成啥樣,子組件不會遞歸渲染;
- Mount: 又稱Full DOM rendering,組件層虛擬Dom與真實Dom的渲染,所以這個方法裏你既能看到Select節點(爲2與組件的定義有關),Option爲0,因爲Antd的Option都是以絕對定位的方式掛載在body節點下的,而非wrapper節點裏。你還可以打印wrapper.find('Trigger')的長度,結果爲1,至於爲什麼,和Select爲2一樣,需要去看Select的源碼;
- Render: 又稱靜態渲染,真實dom節點的渲染,無虛擬Dom,不過有趣的是wrapper節點就是根節點,所以wrapper.find('.render')的length爲0,而且很多前兩者能用的方法在這裏都沒有,比如containsMatchingElement這樣最基本的方法。
組件事件的模擬及事件回調的mock
下面兩節都將以自己最近封裝的一個Antd組件爲例來做說明,在我前面的一篇文章裏有提到,如下圖所示:
這個組件的大致功能如上面圖所示,產品需求就是需要一個編輯框,這個框在用戶點擊輸入時,需要彈出一個搜索框,根據用戶的輸入遠程搜索獲取數據形成一個下拉列表,供用戶選擇,選擇完成後搜索框被收起。而從代碼上,也很簡單:
<div
className="originSearch"
style={style}
ref={el => this.searchInputWrapper = el}
>
<Input
ref={e => this.searchInput = e}
readOnly
placeholder={placeholder}
value={valueFormat(value)}
style={{ width: '100%' }}
size="default"
{...inputProps}
/>
{
isShowSearch &&
<div className="js-origin-search origin-search">
<Icon type="search" className="origin-search-icon" />
<AutoComplete
autoFocus
ref={el => this.searchRealInput = el}
className="certain-category-search"
dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth
onSearch={this.handleChange}
onSelect={this.handleSelect}
style={{ width: '100%' }}
optionLabelProp="value"
>
{loading ? [<Option key="loading" disabled><Spin spinning={loading} style={{ paddingLeft: '45%', textAlign: 'center' }} /></Option>] : options}
</AutoComplete>
</div>
}
</div>
第一個事件,用戶開始輸入,Input被單擊,click事件捕捉,isShowSearch由false變爲true, AutoComplete組件渲染,並自動獲得焦點。
對於這一個測試,這是需要有用戶交互的,和測試點擊瀏覽器,所以我們需要用到模擬事件simulate,看下面代碼:
it('Input state change disable when click', () => {
const wrapper = mount(
<HInputSearch {...searchProps} />
);
wrapper.find('input').simulate('click');
expect(wrapper.state('isShowSearch')).toEqual(true);
const inputNodes = wrapper.find('input');
expect(inputNodes.length).toEqual(2);
});
除了模擬點擊事件,還可以模擬輸入框值的change事件,接着我們還可以檢測這個變化是否觸發了相應的方法,比如下面這段:
it('Input state change disable when click', () => {
const inputValue = 'change';
const change = jest.spyOn(HInputSearch.prototype, 'handleChange'); // handleChange是在定義組件時,定義的一個原型方法
const wrapper = mount(
<HInputSearch {...searchProps} />
);
wrapper.find('input').simulate('click');
expect(wrapper.state('isShowSearch')).toEqual(true);
const inputNode = wrapper.find('.origin-search input');
inputNode.simulate('change', { target: { value: inputValue } });
expect(wrapper.find('input').get(1).props.value).toEqual(inputValue);
expect(change).toBeCalledWith(inputValue); // 這裏可以用toBeCalled檢測是否調用,而使用toBeCalledWith除了檢測是否調用,還可以檢測是否正確的傳參;
});
除了在原型上直接mock響應的方法,也可以直接在實例上,查找出某個節點利用jest.spyOn來檢測某個方法是否被調用。在下一節還會繼續對jest的函數mock進行說明。
異步請求的模擬
此次封裝的案例組件我稱之爲遠程搜索輸入框,所以涉及到防抖與異步請求的發起,所以在Input框值變化時,首先是使用lodash的debounce函數防抖,然後發起請求。所以當我們進行觸發後的流程測試時,比如異步請求是否被調用,返回值是否正常的被存入state,Option是否生成,這些統統沒法立即執行測試,而是需要一段時間的等待再來判斷,我們把這稱之爲異步測試。進行這個測試,先理一理思路:
- 首先: 需要模擬一個異步請求;
- 其次: 需要模擬獲取數據後數據轉換函數format;
- 最後: 模擬一個異步任務,這個簡單,用setTimeout就可以。
來看一下實現:
export const response = [{
name: '李梅梅', id: 12,
}, {
name: '徐雷雷', id: 13,
}, {
name: 'james', id: 14,
}];
export default function fetch() {
return new Promise(resolve =>
setTimeout(() => {
resolve(response);
}, 5000)
);
}
const mockFetch = jest.fn(val => fetch(val));
const mockFormat = jest.fn(data => data.map(({ id, name }, index) => ({
label: `${name}(${id})`,
value: name,
key: index
})));
const searchProps = {
value: initValue,
style: { width: '100%' },
search: {
keyword: undefined,
},
onSelect: mockSelect,
format: mockFormat, // 利用jest.fn() mock的
fetchData: mockFetch, // 利用jest.fn() mock的
};
it('Input state change disable when click', (done) => { // 異步測試必備
const inputValue = 'change';
const change = jest.spyOn(HInputSearch.prototype, 'handleChange'); // handleChange是在定義組件時,定義的一個原型方法
const wrapper = mount(
<HInputSearch {...searchProps} />
);
wrapper.find('input').simulate('click');
const inputNode = wrapper.find('.origin-search input');
inputNode.simulate('change', { target: { value: inputValue } });
expect(change).toBeCalledWith(inputValue); // 這裏可以用toBeCalled檢測是否調用,而使用toBeCalledWith除了檢測是否調用,還可以檢測是否正確的傳參;
setTimeout(() => { // 模擬異步任務
expect(mockFetch).toHaveBeenCalledWith({ keyword: inputValue });
expect(mockFormat).toBeCalled();
done(); // 這個done()很重要,會告訴這個異步測試是否完成
}, 1000);
});
結合上面的實例可以看出,好像異步測試也不是很麻煩,就在測試用例中多了個測試完的回調函數done;異步請求和mock函數其實質都是用jest.fn直接包一下;異步任務可以直接用setTimeout或者setImmediate來模擬,當然也有文藝一點的寫法,比如寫一個通用的:
// 異步任務模擬
function mockPromises() {
return new Promise(resolve => setTimeout(() => {
resolve();
}, yourTime));
}
結語
寫完這一個組件,自己收穫還是非常大的,並不是學了jest或者enzyme這麼多API的使用,說實話,這個意義真不大。主要意義在於不自願的去看了一些Antd組件以及Antd 底層組件的一些實現源碼,它的高階組件應用以及組件拆分的方式讓我還是很有收穫。另外,我其實一直在思考,寫單元測試的意義,因爲開始我以爲這個很神奇,能夠寫一些邏輯什麼的,就能找出自己組件的bug.其實不是,單測只是在你能想到的案列進行描述,然後看是否運行正常,有些小bug也許能找出來,但一些沒想到的應用場景,bug還是在哪裏,並沒有被發現。所以我覺得意義不大,但後面一次優化,改變了我的想法。試想:
- 當你歇了一段時間,也許你自己寫的組件你都看不懂了,但你接到團隊其他人的需求需要優化其中的一部分代碼,你改完覺得沒問題,就push了代碼,但是一上線,發現動了不改動的邏輯,影響了最初的功能。這個時候,單元測試的意義就體現了。你第一次寫了這個組件,並且寫了相應的單元測試,並且代碼測試覆蓋率能達到80%,當你下一次優化時,優化完你只需要再跑一遍以前寫的單元測試(如果是功能性的優化,有必要針對這個補充一個針對性的單元測試),如果測試跑通,你在push代碼,這樣發生bug的概率會顯著下降。
以上就是本篇文章所有,謝謝瀏覽。有什麼描述不嚴謹之處,還請指正。
原文見:地址
源碼及測試用例:地址