前言
測試是應用生產過程中不可缺少的一個環節,開發人員在編碼時總有考慮不周全或者出錯的情況,而測試則是通過對比實際結果與預期結果來找出問題和缺陷,從而確保軟體的質量。本文主要介紹了在最近在工作中用Jest
和Enzyme
來測試React 元件的過程和容易踩坑的地方。
測試種類
對於一個Web網站來說,測試的種類主要分為以下3種:
- 單元測試: 測試單個函式或者類,提供輸入,確保輸出和預期的一樣。單元測試的粒度要儘可能小,不要考慮其他類和模組的實現。
- 整合測試: 測試整個流程或者某元件能夠按預期的執行,用來覆蓋跨模組的過程。同時也要包括一些反面用例。
- 功能測試: 站在產品的角度測試各個場景,通過操作瀏覽器或者網站,忽略內部實現細節和結構,確保和預期的行為一樣。
測試框架
市面上現在有很多測試工具,公司裡採用Umijs作為腳手架快速搭建了一個React應用,而Umi內部採用了Dva作為資料流管理,同時也自動配置了Jest測試框架。
Jest
測試框架由Facebook所推薦,其優點是執行快效能好,並且涵蓋了測試所需的多種功能模組(包括斷言,模擬函式,比較元件的快照snapshot,執行測試,生成測試結果和覆蓋率等),配置簡單方便,適合大專案的快速測試。
React元件的測試
測試React元件我們採用Enzyme
工具庫,它提供3種元件渲染方式:
Shallow
:不會渲染子元件Mount
: 渲染子元件,同時包含生命週期函式如componentDidMount
Render
: 渲染子元件,但不會包含生命週期,同時可用的API也會減少比如setState()
一般情況下用shallow和mount的情況比較多。
被Connect包裹的元件
有些元件被Connect
包裹起來,這種情況不能直接測,需要建立一個Provider
和傳入一個store
,這種過程比較痛苦,最好是將去掉Connect
後的元件 export
出來單獨測,採用shallow
的渲染方法,僅測該元件的邏輯。
例如被測的元件如下:
export class Dialog extends Component {
...
}
export default connect(mapStateToProps, mapDispatch)(Dialog)
複製程式碼
那麼在測試檔案中, 可以這樣初始化一個控制元件:
import {Dialog} from '../dialog'
function setup(propOverrides) {
const props = Object.assign(
{
state:{}
actions:{},
},
propOverrides,
)
const enzymeWrapper = shallow(<Dialog {...props} />)
return {
props,
enzymeWrapper,
}
}
複製程式碼
需和子元件和原生DOM元素互動的元件
有的元件,需要測試和原生DOM元素的互動,比如要測點選原生button元素,是否觸發當前的元件的事件,或者需要測試和子元件的互動時,這時候用需要用mount來渲染。
例如,我的Editor元件是這樣:
export default class Editor extends Component {
constructor(props) {
super(props)
this.state = {
onClickBtn: null,
}
}
handleSubmit = ({ values, setSubmitting }) => {
const { onClickBtn } = this.state
this.props.actions.createInfo(values, onClickBtn)
}
handleCancel = () => {
...
}
setOnClickBtn(name) {
this.setState({
onClickBtn: name,
})
}
render() {
return (
<Form onSubmit={this.handleSubmit}>
{({ handleChange }) => {
return (
<div className="information-form">
<Input name={FIELD_ROLE_NAME} onChange={handleChange}
/>
<Input name={FIELD_ROLE_KEY} onChange={handleChange}
/>
<div>
<Button type="button" onClick={this.handleCancel}> Cancel </Button>
<Button type="submit" primary onClick={() => this.setOnClickBtn('create')} > Create </Button>
<Button type="submit" primary onClick={() => this.setOnClickBtn('continue')} > Create and Continue </Button>}
</div>
</div>
)
}}
</Form>
)
}
}
複製程式碼
此時Form的children是個function
,要測試表單中按鈕點選事件,如果只用shallow,是無法找到Form中children的元素的,因此這裡採用mount
方式將整個dom渲染,可直接模擬type為submit屬性的那個button的點選事件。
然後測試點選該button是否完成了2個事件:handleSubmit
和setOnclickBtn
。
有人會想到模擬form的submit
事件,但在mount的情況下,模擬button的click
事件同樣可以觸發onSubmit事件。
由於submit過程要涉及子控制元件的互動,其過程具有一定的不確定性,此時需要設定一個timeout
,延長一段時間再來判斷submit內的action是否被執行。
it('should call create role action when click save', () => {
const preProps = {
actions: {
createInfo: jest.fn(),
}
}
const { props, enzymeWrapper } = setup(preProps)
const nameInput = enzymeWrapper.find('input').at(0)
nameInput.simulate('change', { target: { value: 'RoleName' } })
const keyInput = enzymeWrapper.find('input').at(1)
keyInput.simulate('change', { target: { value: 'RoleKey' } })
const saveButton = enzymeWrapper.find('button[type="submit"]').at(0)
saveButton.simulate('click')
expect(enzymeWrapper.state().onClickBtn).toBe('save')
setTimeout(() => {
expect(props.actions.createInfo).toHaveBeenCalled()
}, 500)
})
複製程式碼
但是用mount
來渲染也有容易讓人失誤的地方,比如說要找到子元件,可能需要多層.children()
才能找到。在單元測試中,應儘量採用shallow
渲染,測試粒度儘可能減小。
含有Promise的情況
有的元件的函式邏輯中會含有Promise
,其返回結果帶有不確定性,例如以下程式碼段中的auth.handleAuthenticateResponse
,傳入的引數是一個callback函式,需要根據auth.handleAuthenticateResponse
的處理結果是error
還是正常的result
來處理自己的內部邏輯。
handleAuthentication = () => {
const { location, auth } = this.props
if (/access_token|id_token|error/.test(location.search)) {
auth.handleAuthenticateResponse(this.handleResponse)
}
}
handleResponse = (error, result) => {
const { auth } = this.props
let postMessageBody = null
if (error) {
postMessageBody = error
} else {
auth.setSession(result)
postMessageBody = result
}
this.handleLogicWithState(postMessageBody)
}
複製程式碼
在測試時,可用jest.fn()模擬出auth.handleAuthenticateResponse
函式,同時讓它返回一個確定的結果。
const preProps = {
auth: {
handleAuthenticateResponse: jest.fn(cb => cb(errorMsg))
}
}
setup(preProps)
複製程式碼
相關API
enzyme: airbnb.io/enzyme/
Jest: jestjs.io/docs/en/api
自留問題
- 使用mount測試一個包含子元件的父元件,以及父子元件的互動過程時,這種測試叫做UT測試還是CT元件測試?
- 元件snapshot測試是什麼?有無必要?如何測?