您當前的位置:首頁 > 繪畫

手把手教你寫好一個React元件單元測試

作者:由 dog porn 發表于 繪畫時間:2020-06-12

前言

單元測試是保證程式碼質量的常見手段之一,對於 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`] = `“

mockButtonName
”`;

exports[`Button 元件測試 渲染正常 1`] = `“

buttonDefaultName
”`;

那麼當你下次改動元件的渲染邏輯時,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

標簽: jest  元件  Enzyme  mock  測試