手把手教你寫好一個React元件單元測試
前言
單元測試是保證程式碼質量的常見手段之一,對於 React 來說,使用 Jest + Enzyme 這樣的組合是目前比較主流的技術選型,本文記錄了作者在對 React 元件做單元測試的過程中得到的一些實踐經驗,手把手教你如何搭建測試環境,並編寫一個完備的 React 元件單元測試
summary
Part1:測試工程搭建
use cra
without cra
Part2:測試 React 元件
元件渲染是否符合預期
事件點選的回撥函式是否正確執行
業務埋點函式是否被正常呼叫
非同步介面請求
setimeout 等非同步操作是否按預期執行
Part3:最佳化
放到同一個資料夾下
封裝測試中的通用程式碼
setupTests 檔案使用
manual mock
完整程式碼可以從 GitHub 倉庫 coderzzp/react-jest-enzyme 獲取
Part1: 測試工程搭建
Setup with Create React App
setp1 - 建立工程
npx create-react-app test-with-cra
step2 - 新增enzyme 等包
yarn add enzyme enzyme-adapter-react-16 -D
step3 - 在 src/setupTest。js中配置enzyme
// src/setupTests。js
import
{
configure
}
from
‘enzyme’
;
import
Adapter
from
‘enzyme-adapter-react-16’
;
configure
({
adapter
:
new
Adapter
()
});
Done!
可以在terminal中使用 yarn test 開始測試了
Setup without Create React App
setp1 - 配置 setupfile
在package。json檔案中宣告jest 配置,注意 jest 這個 key 要放在 name 正下方
{
“name”
:
“my-react-app”
,
“jest”
:
{
“setupFiles”
:
[
“。/setupTest”
]
}
。。。
}
step2 - 新增enzyme ,jest 等包
yarn add enzyme enzyme-adapter-react-16 jest -D
Step3 - 在根目錄下建立 setupTest。js 檔案
// src/setupTests。js
import
{
configure
}
from
‘enzyme’
;
import
Adapter
from
‘enzyme-adapter-react-16’
;
configure
({
adapter
:
new
Adapter
()
});
step4 - 在package。json檔案中配置script
{
“scripts”
:
{
“test”
:
“jest”
}
}
PS: 對於babel 配置的寫法,取決於具體專案,可以查閱 babel 官方文件 來獲取更多詳細資訊
Done!
Part2: 測試 React 元件
如下是一個React 元件,它包含了一些我們業務中經常出現的業務邏輯,例如點選事件,非同步介面等等
import
React
from
‘react’
;
import
fetchData
from
‘。/services’
import
tracker
from
‘。/tracker’
export
default
class
Button
extends
React
。
Component
{
state
=
{
buttonName
:
‘buttonDefaultName’
,
data
:
{},
// 服務端資料
}
constructor
(
props
){
super
(
props
)
// 埋點
tracker
。
page
(
‘button init’
)
}
componentWillMount
(){
this
。
timer
=
setTimeout
(()
=>{
this
。
setState
({
buttonName
:
‘buttonAfter3seconds’
})
},
3000
)
}
async
componentDidMount
(){
const
res
=
await
fetchData
()
this
。
setState
({
data
:
res
})
}
onButtonClick
=
()
=>{
this
。
props
。
onButtonClick
&&
this
。
props
。
onButtonClick
()
}
render
(){
const
{
buttonName
}
=
this
。
state
return
<
div
>
<
div
className
=
“button”
onClick
=
{
this
。
onButtonClick
}
>
{
this
。
props
。
buttonName
||
buttonName
}
<
/div>
<
/div>
}
}
我們的測試目的會圍繞這幾個關鍵點:
元件是否符合預期的渲染了?
事件點選的回撥函式是否正確執行了?
業務埋點函式是否被正常呼叫了?
非同步介面請求如何校驗?
setimeout 等非同步後的操作如何校驗?
1。 元件是否符合預期的渲染了
不同的外界條件會造成不同的 UI 渲染結果,例如props不同,UI渲染結果也是不同的
propsA → Component Render → renderResultA
propsB → Component Render → renderResultB
我們可以透過Enzyme去渲染元件,再透過snapshot能力對 renderResultA / B 進行一個快照
在 button 元件同級新增
__test__/button1。test。js
檔案
//__test__/button1。test。js
// snapshot
import
React
from
‘react’
import
{
shallow
}
from
‘enzyme’
;
describe
(
‘Button 元件測試’
,
()
=>
{
it
(
‘渲染正常’
,
()
=>
{
const
Button
=
require
(
‘。。/button’
)。
default
// enzyme的shallow方法用於渲染元件
const
componentWrapper
=
shallow
(
<
Button
/>
);
expect
(
componentWrapper
。
html
())。
toMatchSnapshot
();
});
it
(
‘props傳入buttonName,渲染正常’
,
()
=>
{
const
Button
=
require
(
‘。。/button’
)。
default
const
componentWrapper
=
shallow
(
<
Button
buttonName
=
{
‘mockButtonName’
}
/>
);
expect
(
componentWrapper
。
html
())。
toMatchSnapshot
();
});
});
之後執行yarn test ,jest會自動為你生成快照檔案,生成不同條件下的結果
// Jest Snapshot v1, https://goo。gl/fbAQLP
exports[`Button 元件測試 props傳入buttonName,渲染正常 1`] = `“
exports[`Button 元件測試 渲染正常 1`] = `“
那麼當你下次改動元件的渲染邏輯時,snapshot會提醒你元件渲染與之前不一致了,如果是你預期內的改動,可以更新你的snapshot檔案,如果不是預期內的改動,那麼你就需要看看是哪裡出了問題
2。 事件點選的回撥函式是否正確執行了
對於元件中的一些事件行為,可以透過Enzyme 渲染出的元件可以模擬,並斷言結果
Component Render → Event simulate → expect result
//__test__/button2。test。js
// simulate click
import
React
from
‘react’
import
{
shallow
}
from
‘enzyme’
;
describe
(
‘Button 元件測試’
,
()
=>
{
it
(
‘click點選事件測試,mockfn校驗’
,
()
=>
{
// 生成一個mockfn,用於校驗點選後的props。func是否被呼叫
const
mockClick
=
jest
。
fn
()
const
Button
=
require
(
‘。。/button’
)。
default
const
componentWrapper
=
shallow
(
<
Button
onButtonClick
=
{
mockClick
}
/>
);
// 模擬點選事件
componentWrapper
。
find
(
‘。button’
)。
at
(
0
)。
simulate
(
‘click’
)
// 校驗mockfn被呼叫
expect
(
mockClick
)。
toHaveBeenCalled
();
});
});
jest。fn() 用來生成mock函式,可以透過mockClick 這個控制代碼去判斷函式的呼叫情況
enzyme提供了類似於jquery的dom選擇器,並透過 simulate(event) 的方式來模擬事件
3。 業務埋點函式是否被正常呼叫了
我們可以看到元件透過模組的方式引入了一個 tracker 函式,用於在元件初始化的時候觸發埋點行為,那如何驗證tracker是否被正常呼叫呢?
使用jest。domock() 來mock 模組
mock module → Component Render → expect module tobe called
//__test__/button3。test。js
// jest。doMock
import
React
from
‘react’
import
{
shallow
}
from
‘enzyme’
;
describe
(
‘Button 元件測試’
,
()
=>
{
it
(
‘校驗埋點是否被正常呼叫’
,()
=>
{
// 生成一個mock函式
const
page
=
jest
。
fn
()
// 宣告mock tracker這個模組,並在第二個引數傳入mock方法
jest
。
doMock
(
‘。。/tracker’
,()=>{
return
{
page
}
})
const
Button
=
require
(
‘。。/button’
)。
default
shallow
(
<
Button
/>
);
// mock函式被呼叫,並且引數是 ‘button init’
expect
(
page
)。
toHaveBeenCalledWith
(
‘button init’
)
});
});
使用jest。doMock() 來 mock 引入的模組
4。 非同步介面請求
我們已經學會了mock module,對於非同步的介面請求,如何校驗我們拿到的資料呢?
jest 已經支援了async/await
//__test__/button4。test。js
// mock async function
import
React
from
‘react’
import
{
shallow
}
from
‘enzyme’
;
describe
(
‘Button 元件測試’
,
()
=>
{
it
(
‘介面測試’
,
async
()
=>
{
jest
。
doMock
(
‘。。/services’
,()=>{
return
()=>{
return
‘mockdata’
}
})
const
Button
=
require
(
‘。。/button’
)。
default
const
componentWrapper
=
await
shallow
(
<
Button
/>
);
// 透過enzyme渲染的元件可以透過 。state() 方法拿到元件 state 狀態
expect
(
componentWrapper
。
state
(
‘data’
))。
toEqual
(
‘mockdata’
)
});
});
由於是在 componentDidMount 中的非同步方法,我們需要在渲染前使用 await ,這樣才能保證斷言中的元件是已經拿到介面資料的
5。setimeout 等非同步操作是否按預期執行了
我們可以看到componentWillMount 生命週期中,元件三秒後的state狀態會發生改變,那麼如何測試到三秒後的狀態呢?在測試中等三秒嗎?顯然不是,我們可以使用jest 提供的faketimer 來幫我們快進時間
jest。useFakeTimers() → Component render → jest。runAllTimers() → expect result
//__test__/button5。test。js
// mocktimer
import
React
from
‘react’
import
{
shallow
}
from
‘enzyme’
;
jest
。
useFakeTimers
()
describe
(
‘Button 元件測試’
,
()
=>
{
it
(
‘3秒後渲染正常’
,
()
=>
{
const
Button
=
require
(
‘。。/button’
)。
default
const
componentWrapper
=
shallow
(
<
Button
/>
);
// 快速執行所有的 macro-task (eg。 setTimeout(), setInterval())
jest
。
runAllTimers
()
expect
(
componentWrapper
。
html
())。
toMatchSnapshot
();
});
});
React元件的測試點基本就是圍繞以上幾個角度,jest 提供了多種斷言api,可以在對應的場景中使用
Part3:最佳化
放到同一個資料夾下
之前的測試程式碼都是針對button組建的測試,它們顯然都需要放到一個測試檔案下,我們可以嘗試將上面的測試程式碼都放到一個檔案下
describe
(
‘Button 元件測試’
,
()
=>
{
it
(
‘測試1xxx。。。’
,
()
=>
{
。。。
});
it
(
‘測試2xxx。。。’
,
()
=>
{
。。。
});
more
it
。。。
});
但如果直接這麼做你會發現,程式碼並不會透過測試,原因是在當上一個測試執行後,這個模組會被快取起來,元件快取的module,props,都有可能會對其他測試用例產生影響,所以我們通常會在 beforEach 這個鉤子函式中使用 jest。resetModules() 來消除不同測試用例之間的影響
describe
(
‘Button 元件測試’
,
()
=>
{
beforeEach
(()=>{
jest
。
resetModules
();
})
it
(
‘測試1xxx。。。’
,
()
=>
{
。。。
});
more
it
。。。
});
常見的鉤子還有: afterEach,beforeAll,afterAll ,會在測試的不同宣告週期執行,詳見
封裝測試中的通用程式碼
觀察上面的程式碼,最常見的複用程式碼就是元件引入 & 生成程式碼,因此我們可以把這部分程式碼封裝起來
// 通用程式碼
const
Button
=
require
(
‘。。/button’
)。
default
const
componentWrapper
=
shallow
(
<
Button
onButtonClick
=
{
mockClick
}
/>
);
可以封裝為 generateComponent 使用,可以設定initprops和需要覆蓋的props
const
generateComponent
=
props
=>
{
const
initProps
=
{};
const
Button
=
require
(
‘。。/button’
)。
default
return
shallow
(
<
Button
{。。。
initProps
}
{。。。
props
}
/>
);
};
這樣我們在每個it語句開頭前,可以使用 generateComponent 來渲染並拿到元件
it
(
‘渲染正常’
,
()
=>
{
const
componentWrapper
=
generateComponent
()
expect
(
componentWrapper
。
html
())。
toMatchSnapshot
();
});
setupTests 檔案使用
setupTests。js 檔案會在測試開始前執行,你可以透過它來配置一些全域性變數
例如,在 setupTests。js 配置全域性引入React 和 shallow 方法,這樣一來,我們的測試程式碼都不需要再重複引入這兩個變數
// setupTests。js
import
{
configure
}
from
‘enzyme’
;
import
Adapter
from
‘enzyme-adapter-react-16’
;
import
React
from
‘react’
import
{
shallow
}
from
‘enzyme’
;
global
。
React
=
React
global
。
shallow
=
shallow
configure
({
adapter
:
new
Adapter
()
});
配置後我們便可以去掉測試程式碼最上方的 React和shallow 模組
// import React from ‘react’
// import { shallow } from ‘enzyme’;
Manual mock
我們已經學習了透過 jest。doMock 來模擬掉元件中的引入的其他模組,對於一些常見的需要被mock掉的模組,例如資料庫操作、fs 等模組,我們可以使用jest 提供的Manual mock,它可以讓你在測試元件時去預設引入你對這些三方模組的mock檔案
Mocking user modules
以常用的埋點 tracker 模組為例,在 tacker 模組同級目錄下增加
__mocks__
檔案
// __mocks__/tracker。js
const
tracker
=
{
page
:
jest
。
fn
()
}
export
default
tracker
對應的測試埋點程式碼為:
// 在測試檔案頭部宣告 jest。mock(‘。。/tracker’),表示我們使用manual mock 來模擬tracker這個檔案
jest
。
mock
(
‘。。/tracker’
)
describe
(
‘Button 元件測試’
,
()
=>
{
it
(
‘校驗埋點是否被正常呼叫’
,()
=>
{
// 獲取 mock 函式 page 的控制代碼
const
{
page
}
=
require
(
‘。。/tracker’
)。
default
generateComponent
()
expect
(
page
)。
toHaveBeenCalledWith
(
‘button init’
)
});
}
Mocking Node modules
對於node_modules中的模組,例如我們需要mock 掉 fs 模組,如下是我們的示例目錄
。
├── config
├── __mocks__
│ └── fs。js
│
├── node_modules
└── views
這樣當我們的模組引入fs模組時,測試檔案會預設使用
__mocks__
檔案下的 fs 檔案,並且無需顯示宣告:
jest。mock(
參考資料:
https://
blog。usejournal。com/@wa
suradananjith