最簡單的測試
jest 是 Facebook 推出的測試工具,enzyme 是airbnb 推出的 React 測試類庫,使用兩者可以很好地測試 React 元件。
首先安裝對應的依賴:
npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-react
複製程式碼
其中 babel-jest
是自動使用 babel 編譯檔案。
安裝 enzyme 相關的依賴:
npm i -D enzyme enzyme-adapter-react-16 jest-environment-enzyme
複製程式碼
配置 enzyme,新建檔案 setupEnzyme.js
,寫入以下內容:
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({
adapter: new Adapter()
})
複製程式碼
配置相應的 jest,
{
"setupFilesAfterEnv": [
"./setupEnzyme.js"
],
"testEnvironment": "enzyme"
}
複製程式碼
配置對應的 babel:
{
"presets": [
["@babel/preset-env"],
["@babel/preset-react"]
]
}
複製程式碼
配置 npm scripts:
"scripts": {
"test": "jest --config=jest.config.json"
},
複製程式碼
以一個最簡單的 list 元件為例:
import React from 'react'
export default ({list}) => {
return <ul>
{
list.map(item => item > 5 && <li className="item" key={item}>{item}</li>)
}
</ul>
}
複製程式碼
比如,傳遞一組 list
資料 [1,2,3,4,5,6]
那麼這個元件應該是渲染 1 個 list item,而如果傳遞資料 [6,7,8,9]
則應該是渲染 4 個 list item,如下:
import React from 'react'
import { render } from 'enzyme'
import List from './index'
describe('<List/>', () => {
it('render 1 child', () => {
const wrapper = render(<List list={[1,2,3,4,5,6]} />)
expect(wrapper.find('.item').length).toBe(1)
})
it('render 4 child', () => {
const wrapper = render(<List list={[6,7,8,9]} />)
expect(wrapper.find('.item').length).toBe(4)
})
})
複製程式碼
執行測試命令就可以看到測試通過的介面。
With redux
使用 Redux 時,connected 元件一種簡單的單元測試方式就是將 plain 元件也 export,如下:
import React from 'react'
import { connect } from 'react-redux'
export const List = ({list}) => {
return <ul>
{
list.map(item => item > 5 && <li className="item" key={item}>{item}</li>)
}
</ul>
}
export default connect((state) =>({list:state.list}))(List)
複製程式碼
這樣就可以像之前的測試一樣測試了。
還有一種方式就是使用 redux-mock-store
,安裝完成後,以一個簡單的例子,如下:
import React from 'react'
import { mount } from 'enzyme'
import configureStore from 'redux-mock-store'
import { Provider } from 'react-redux'
import List from './index'
const mockStore = configureStore([])
describe('<List/>', () => {
it('render 0 child', () => {
const store = mockStore({
list: []
})
const wrapper = mount(<Provider store={store}><List/></Provider>)
expect(wrapper.find('.item').length).toBe(0)
})
it('render 2 child', () => {
const store = mockStore({
list: ['111','222']
})
const wrapper = mount(<Provider store={store}><List/></Provider>)
expect(wrapper.find('.item').length).toBe(2)
})
})
複製程式碼
這裡需要注意的有兩點:
redux-mock-store
只是為了測試 actions 相關的邏輯,不會自動更新 store,(所以上面的第二個測試沒有直接使用store.dispatch
,而是重新 mock 了資料,因為作者認為 reducer 就是純函式,純函式怎麼測試就怎麼測試,see github.com/arnaudbenar…)- 第二個就是使用了
mount
而不是使用shallow
因為shallow
只渲染當前元件,只能能對當前元件做斷言;mount
會渲染當前元件以及所有子元件,顯然上面是需要使用mount
的
with state
單元測試需要明確的一點是不應該去測試實現的邏輯,而是應該關注輸入輸出,而 state 的改變基本上最終都會體現在 UI 的改變,所以關注 UI 的改變即可,以下例子,點選按鈕後,state 內容改變:
import React, { useState } from 'react'
const Add = () => {
const [text, setText] = useState('hello')
return (
<div>
<button onClick={() => setText(text === 'hello' ? 'world' : 'hello')}>change</button>
<p>{text}</p>
</div>
)
}
export default Add
複製程式碼
那麼他的測試應該是測試點選後文字內容是否更改,如下:
import React from 'react'
import { shallow } from 'enzyme'
import Add from './index'
describe('<Add/>', () => {
it('click', () => {
const wrapper = shallow(<Add/>)
expect(wrapper.find('p').text()).toBe('hello')
wrapper.find('button').simulate('click')
expect(wrapper.find('p').text()).toBe('world')
wrapper.find('button').simulate('click')
expect(wrapper.find('p').text()).toBe('hello')
})
})
複製程式碼
UI互動
對於使用者介面的操作, enzyme 可以通過 simulate
來模擬互動事件,以一個簡單的例子為例,使用者點選按鈕後觸發事件更新store:
import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import Add from './index'
const mockStore = configureStore([])
describe('<Add/>', () => {
it('click', () => {
const store = mockStore({
list: []
})
const wrapper = mount(<Provider store={store}><Add/></Provider>)
wrapper.find('button').simulate('click')
wrapper.find('button').simulate('click')
expect(store.getActions().length).toBe(2)
})
})
複製程式碼
這裡利用的就是使用者點選兩次後將觸發兩次 dispatch,通過 store.getActions
來判斷是否觸發 dispatch,以及 dispatch 的次數。
snapshot
snapshot 快照測試第一次執行的時候會將 React 元件在不同情況下的渲染結果儲存一份快照檔案。後面每次執行快照測試的時候,都會和第一次比較,想要生成新的快照檔案新增 -u
引數生成新的快照檔案。快照檔案是以 .snap
結尾的檔案,會在執行測試的時候存放在 __snapshots__
資料夾下。
snapshot 測試 jest 提供了 react-test-renderer
, enzyme 提供的 render
進行了封裝,同時提供了 enzyme-to-json
幫助將 wrapper
與快照檔案進行對比,以一個簡單的例子為例:
it('basic use', () => {
const text = ['12', '13']
const wrapper = render(
<List list={text} />
)
expect(toJson(wrapper)).toMatchSnapshot()
})
it('without item', () => {
const wrapper = render(
<List list={[]}/>
)
expect(toJson(wrapper)).toMatchSnapshot()
})
複製程式碼
第一次執行測試後會生成對應快照檔案:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<List/> basic use 1`] = `
<ul>
<li
class="item"
>
12
</li>
<li
class="item"
>
13
</li>
</ul>
`;
exports[`<List/> without item 1`] = `<ul />`;
複製程式碼
修改元件後,重新測試則會報錯:
如果你確定這次的修改是符合你預期的,那麼你應該重新生成快照檔案。
快照檔案應該被 git 提交跟蹤嗎?當然,快照檔案應該是要和程式碼一併提交和 review 的。
快照檔案更新問題,如果一段時間大量的修改了很多 UI 元件,這個時候控制檯就會有很多錯誤,這個時候需要單獨看每個元件是否符合我們的更改,符合的話就需要重新生成快照, -u
引數是會預設更新所有的快照的,如果只是想要更新部分,可以使用 --testNamePattern
引數。
當然開發的時候應該是儘可能多提交 git,並且把單測放在 git hooks 上,儘量避免需要一次審查過多檔案的情況。
with TypeScript
遷移到 Typescript 後將 babel-jest 替換為 ts-jest 即可。
npm i ts-jest -D
複製程式碼
參考:
- 使用jest+enzyme進行react專案測試 - 測試手法篇
- enzyme_render_diffs.md
- 使用 Enzyme 進行 React 元件測試進階
- 用jest+enzyme來寫Reactjs的單元測試吧!
- Testing React / Redux Apps with Jest & Enzyme
最後照舊是一個廣告貼,最近新開了一個分享技術的公眾號,歡迎大家關注?(目前關注人數可憐?)