前言:有了前面Jest和Enzyme基礎知識的儲備,接下來,我們看如何在項目中測試組件。
一.測什麼和如何測?
當我們看到一個組件時,首要考慮測試什麼?然後纔是如何測試。作爲一個通用的測試原則,我是基於這幾個方面進行考慮。
下面所有測試代碼,都是針對TodoList這個示例demo,界面如下:
功能說明:在Add Todo這個input框中輸入內容,點擊回車時,內容添加到正在進行這個列表中,同時還要進行個數統計,並且清空input框。
-
組件節點測試
其實就是隻關注render函數中,組件的渲染情況就可以了。
上面的render函數是有輸入框的Header組件,接下來我們根據渲染,編寫測試用例。
雖然測試用例測試的方面,組件渲染出來時,我們一眼就能看出來,是否滿足需求,但還是建議編寫測試代碼。因爲,當我們項目非常龐大時,很可能在不經意間修改了代碼,比如修改了placeholder,我們可能不容易發現這種修改,這時候,測試用例就會不通過,除非我們真的想要改變placeholder,此時,可以修改測試用例,讓測試通過。這樣,就可以很好的避免誤修改。 -
交互測試
主要是利用simulate()函數模擬事件,實際上simulate是通過觸發事件綁定函數,來模擬事件的觸發。觸發事件後,去判斷props上特定函數是否被調用,傳參是否正確;組件狀態是否發生預料之中的修改;某個dom節點是否存在,是否符合期望。
import React, { Component } from 'react';
class Header extends Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
this.state = {
value: ''
}
}
handleInputChange(e) {
this.setState({
value: e.target.value
})
}
handleInputKeyUp(e) {
const { value } = this.state;
if (e.keyCode === 13 && value) {
this.props.addUndoItem(value);
this.setState({ value: '' });
}
}
render() {
const { value } = this.state;
return (
<div className="header">
<div className="header-content">
TodoList
<input
placeholder="Add Todo"
className='header-input'
data-test='input'
value={value}
onChange={this.handleInputChange}
onKeyUp={this.handleInputKeyUp}
/>
</div>
</div>
)
}
}
export default Header;
Header組件上有兩個事件處理函數,現在通過交互測試,測試特定事件觸發時,這兩個函數的行爲是否符合預期。
注意:上面的外部傳入函數,addUndoItem,是通過jest.fn()這種形式去模擬的,這是因爲,我們做的只是Header組件的單元測試,我們只要能夠確定,回車時,外部函數被調用了就可以了,至於調用的結果,todoItem是否添加成功,不是Header需要關心的,這是傳入這個函數的組件需要關心的。
- 測試組件的生命週期函數
由於生命週期函數是在組件的某一時刻自動調用執行的,是一種自觸發的行爲,所以,我們進行監控。
it('componentDidMount called', () => {
const componentDidMountSpy = jest.spyOn(TodoList.prototype, 'componentDidMount');
const wrapper = shallow(<TodoList />);
expect(componentDidMountSpy).toHaveBeenCalled();
componentDidMountSpy.mockRestore();
})
注意點:
使用spy替身的時候,在測試用例結束後,要對spy進行restore,不然這個spy會一直存在,並且無法對相同的方法再次進行spy。
使用spyOn來mock 組件的componentDidMount,替身函數要在組件渲染之前,所有替身函數要定義在shallow(<TodoList />)執行之前。
由於生命週期函數是通過componentDidMount()這種形式定義的,所以要對組件的prototype進行mock。
如果僅僅是隻測試組件的生命週期函數,那意義不是很大,就像上面我們只是測試生命週期函數是否執行了。通常我們會有這樣的需求,比如:比如componentDidMount中發送ajax請求,獲取數據。
但在真實的項目測試中,我們並不會去真實的請求後臺接口,用真實的數據測試,因爲這樣會拉低測試的速度。一般,都是和後臺定好契約,然後在測試時,使用mock。
具體怎麼做呢?
上面我們使用axios這個庫進行請求的發送。因此,我們可以mock(模擬)axios。新建一個__mocks__文件夾,然後在裏面新建axios.js文件。模擬代碼如下:
const mockUndoList = {
data: [{
status: 'div',
value: 'afeng hello'
}],
success: true
};
export default {
get(url) {
if (url === '/undoList.json') {
return new Promise((resolve, reject) => {
if (this.success) {
resolve(mockUndoList)
} else {
reject(new Error());
}
});
}
}
}
測試代碼:
import axios from '../../__mocks__/axios';
beforeEach(() => {
axios.success = true;
})
it(`
1. 用戶打開頁面,請求正常
2. 應該展示接口返回的數據
`, (done) => {
const wrapper = mount(
<Provider store={store}><TodoList /></Provider>
);
process.nextTick(() => {
wrapper.update();
const listItem = findTestWrapper(wrapper, 'list-item');
expect(listItem.length).toBe(1);
done();
})
});
測試時,引入mock的axios,這樣組件渲染時,裏面的axios就是我們自已定義的。做到對請求的攔截處理。
注意:上面代碼中,使用了process.nextTick(),這是因爲請求是異步的,我們不能直接在組件渲染後,就去驗證是否達到預期。所以,使用這個或者setTimeout(fn, 0);等請求回來了,setState調用之後,再去驗證。
- 快照測試
快照會生成一個組件的UI結構,並用字符串的形式存放在__snapshots__文件裏,通過比較兩個字符串來判斷UI是否改變,因爲是字符串比較,所以性能很高。
要使用快照功能,可以使用enzyme的render靜態渲染一下expect(render(<Header />)).toMatchSnapshot();
,jest在執行的時候如果發現toMatchSnapshot方法,會在同級目錄下生成一個__snapshots文件夾用來存放快照文件,以後每次測試的時候都會和第一次生成的快照進行比較。可以使用jest --updateSnapshot來更新快照文件。
應用場景:對於一些公用的組件和UI組件,由於變化的可能性很小,所以,我們可以使用快照固化這個組件。只有當確切的知道修改了,在去更新快照。
二.單元測試和集成測試
首先,瞭解一下。TDD和BDD。
TDD(Testing Driven Development):測試驅動開發。強調的是一種開發方式。以測試來驅動整個項目的開發。即先根據接口,編寫測試用例。然後完成功能開發,目的是要通過所有測試。
一般結合單元測試一起使用。單元測試就是對一個模塊或者組件進行的測試。基本特徵就是隻要輸入不變,必定返回同樣的輸出。一個軟件越容易些單元測試,就表明它的模塊化結構越好,給模塊之間的耦合越弱。React的組件化和函數式編程,天生適合進行單元測試。
優點:
長期減少迴歸bug。
代碼質量更好(組織,可維護性好)
測試覆蓋率高
錯誤測試代碼不容易出現
BDD(Behavior Driven Development):行爲驅動開發。強調的是寫測試的風格,即測試要寫的像自然語言。與TDD的不同,BDD先編寫業務代碼,然後在編寫測試用例。
一般結合集成測試一起使用,集成測試,就是在單元測試的基礎上,將所有子組件組合起來,拼裝成系統或者子系統,然後,測試組合起來,所有的功能是否達到預期。集成測試,一般都是模擬用戶的行爲,是以story的形式進行的。
it(`
1. Header 輸入框輸入內容
2. 點擊回車
3. 列表中展示用戶輸入的內容項
`, () => {
const wrapper = mount(
<Provider store={store}><TodoList /></Provider>
);
const inputElem = findTestWrapper(wrapper, 'header-input');
const content = "afeng"
inputElem.simulate('change', {
target: { value: content }
});
inputElem.simulate('keyUp', {
keyCode: 13
});
const listItem = findTestWrapper(wrapper, 'list-item');
expect(listItem.length).toEqual(1);
expect(listItem.text()).toContain(content);
});
三.總結
說了這麼多,是時候總結一下,最佳實踐了。
一般而言,當我們開發一個個小的組件時,建議進行單元測試,首先保證,外部調用時,這個組件是好用的。
當我們將這些組件,組合在一起使用時,建議進行集成測試,保證用戶行爲發生時,整個流程是能跑通的。