前言
以前單元測(cè)試在JavaScript項(xiàng)目中配置其實(shí)還是挺繁瑣的,依賴各種庫mocha,chai,sion或者第三方覆蓋率報(bào)表生成庫,但是現(xiàn)在Facebook推出了Jest測(cè)試框架,并在react native項(xiàng)目初始化時(shí)就已經(jīng)集成了該環(huán)境,所以還沒玩過的同學(xué)們可以耐心的看下去,說不定玩一次就愛上了寫單元測(cè)試呢。
Jest框架
Jest已經(jīng)內(nèi)置了斷言,mock方案,以及異步處理(async/await),只需簡單配置即可導(dǎo)出代碼覆蓋率報(bào)告,還有針對(duì)于UI的快照測(cè)試。官方聲稱的
Delightful JavaScript Testing
??
環(huán)境配置
因?yàn)槭腔赿va框架開發(fā)的react native項(xiàng)目,所以我們著重測(cè)試model類的方法(reducers和effects)
- package.json中針對(duì)jest的配置
"jest": {
"preset": "react-native",
"collectCoverage": true,
"coverageReporters": [
"lcov"
],
"transformIgnorePatterns": [
"node_modules/(?!react-native|react-navigation)"
],
"moduleNameMapper": {
"react-native": "<rootDir>/mocks/react-native.js"
}
collectCoverage
是否開啟跑測(cè)試代碼時(shí)收集覆蓋率
coverageReporters
導(dǎo)出報(bào)告文件類型(通過該導(dǎo)出的文件和上傳到sonar分析)
transformIgnorePatterns
transformIgnorePatterns
將一些model中涉及到的npm進(jìn)行babel轉(zhuǎn)換,不然在測(cè)試中無法識(shí)別es6的語法
moduleNameMapper
指定需要mock庫對(duì)應(yīng)的mock文件
如何寫一個(gè)測(cè)試代碼
首先,介紹下這個(gè)model的reducr和effect方法的功能(具體dva的model怎么寫,可以github下,這里不多篇幅講解)。reducers中的changeLoginStatus很簡單就是根據(jù)payload的對(duì)象改變state中對(duì)應(yīng)的key;而effects中的login方法(注:這是一個(gè)generator)就是根據(jù)請(qǐng)求體payload中的參數(shù)進(jìn)行網(wǎng)絡(luò)請(qǐng)求,這里我已經(jīng)封裝成一個(gè)方法了,根據(jù)返回的response來調(diào)用對(duì)應(yīng)的action,從而改變state。
login.js
import { NativeModules} from 'react-native'
import { NavigationActions } from '../../utils'
import quickLogin from '../../utils/userAccount'
import Toast from '../../utils/Toast'
import {fetchisCompletedUserInfo} from '../fill-information/server'
import {
fetchUserInfoAndUpdateLocal
} from '../user-info/server'
const {
YCUserInfoPlugin,
} = NativeModules
const accountInfo = {
phoneNum: 18581111111,
code: 11111
}
export default {
namespace: 'login',
state: {
isLogin: false,
failReason: null
},
reducers: {
changeLoginStatus(state, {payload}) {
return {
...state,
isLogin: payload.isLogin,
failReason: payload.failReason
}
}
},
effects: {
* login({payload}, { call, put }) {
try {
const res = yield call(quickLogin, payload.phoneNum, payload.code)
if (res.succeed) {
yield call(YCUserInfoPlugin.setUserToken, res.data)
yield put({ type: 'changeLoginStatus', payload: {
isLogin: true
}})
} else {
yield put({ type: 'changeLoginStatus', payload: {
isLogin: false,
failReason: 'test-failReason'
}})
}
} catch (error) {
global.log(error)
}
}
}
}
主要就是測(cè)試reducer和effect方法
login-test.js
describe('LoginModel------------>reducer', () => {
it('changeLoginStatus -> state all key should change to setvalue', () => {
// reduce 參數(shù)1:state初始值;參數(shù)2:action
expect(reduces.notifyVerificatioStatus(
{...payload},
{type: 'changeLoginStatus', payload: {
isLogin: false,
failReason: 'test-failReason'
}}
)).toEqual({...payload, isLogin: false, failReason: 'test-failReason'})
})
})
describe('LoginModel------------>effects', () => {
it('login -> login success with phone number', () => {
// Given
const {call, put} = effects
const saga = quickLogin.effects.login
const actionCreator = {
type: 'login',
payload: {
...accountInfo
}
}
// When
const generator = saga(actionCreator, {call, put})
generator.next()
generator.next({
succeed: true,
data: 'Test-User-Token'
})
const changeLoginStatus = generator.next()
const end = generator.next()
// Then
expect(changeLoginStatus.value).toEqual(put({
type: 'changeLoginStatus',
payload: {
isLogin: true
}}))
expect(end.done).toEqual(true)
})
})
其中yield call(YCUserInfoPlugin.setUserToken, res.data)
這是調(diào)用一個(gè)NativeModule方法,在執(zhí)行測(cè)試的時(shí)候,你可能會(huì)發(fā)現(xiàn)會(huì)報(bào)找不到Y(jié)CUserInfoPlugin的setUserToken方法,各位看官不急,因?yàn)檫@個(gè)是寫在native的,我們也不需要關(guān)系它是否正確,只需知道調(diào)用了這句話即可,我們可以把它mock掉。怎么做能?
- 方法一:可以直接在當(dāng)前測(cè)試文件,在import前執(zhí)行如下代碼:
jest.mock('react-native', () => {
NativeModule: {
YCUserInfoPlugin: {
setUserToken: () => {}
}
}
})
import ...
import ...
code
- 方法二:在創(chuàng)建一個(gè)名為mocks的文件夾,因?yàn)樾枰猰ock的react-native包中NativeModule對(duì)象中的YCUserInfoPlugin,所以創(chuàng)建創(chuàng)建文件為react-native.js,然后在package.json的moduleNameMapper中配置改文件的路徑,即
包名: '文件所在的路徑'
mocks/react-native.js
export default const NativeModules = {
YCUserInfoPlugin: {
setUserToken: () => {}
}
}
這樣jest就知道在跑測(cè)試代碼時(shí),去找我們mock的文件了,test case 也可以順利跑過了。因?yàn)檫@個(gè)測(cè)試用例中只需要知道那句代碼執(zhí)行就ok啦。
測(cè)試代碼解析
在執(zhí)行單個(gè)測(cè)試用例的時(shí)候,有可能會(huì)遇到全局設(shè)置的問題,你可以在beforeAll()或是在afterAll()周期方法中做一些初始化和回滾現(xiàn)場(chǎng)的操作。
一般來說我們主要測(cè)試數(shù)據(jù)交互的模塊,所以model就是重點(diǎn),正常來說我們網(wǎng)絡(luò)請(qǐng)求這塊是需要mock掉的,但是因?yàn)樵赿va框架中,我們一般把網(wǎng)絡(luò)請(qǐng)求封裝在effects中,而且這個(gè)方法是個(gè)generator函數(shù)(dva框架集成的redux-saga),我們可以很方面的在里面的每一個(gè)yeild語句里自定義返回值,就可以設(shè)置不同類型的返回值,來執(zhí)行不同的語句覆蓋。
使用體驗(yàn)吐槽
jest中針對(duì)于測(cè)試替身這塊的能力還是沒有Sinon厲害,而且API又少,文檔有誤導(dǎo) 性,想要更深入的寫一些測(cè)試用例還得借助第三方的包。
Sinon介紹
當(dāng)你在寫測(cè)試代碼中不順利的時(shí)候,或是把其中的代碼變?yōu)闇y(cè)試替身,絕對(duì)是一個(gè)不二選擇。下面可以看下簡單的測(cè)試用例,來了解下Sinon的幾大概念。
person.js
export default class Person {
static say(message) {
console.log('person say ', message)
}
static eat(food) {
return `person eat ${food}`
}
static save(name) {
console.log(`person saved -> ${name}`)
}
}
person-test.js
import Person from '../person'
import sinon from 'sinon'
describe('sinon test', () => {
it('spy', () => {
const message = 'hello world'
const spy = sinon.spy(Person, 'say')
Person.say(message)
expect(spy.withArgs(message).calledOnce).toEqual(true)
spy.restore()
})
it('stub', () => {
const message = 'hello world'
const returnValue = 'stub eat apple'
sinon.stub(Person, 'say').callsFake((message) => {
console.log(`stub log ${message}`)
})
const stub = sinon.stub(Person, 'eat')
stub.withArgs('apple').returns('stub eat apple')
const result = Person.eat('apple')
expect(stub).toEqual(returnValue)
stub.restore()
})
it('mock', () => {
const name = 'yellow'
const mock = sinon.mock(Person)
mock.expects('save').once().withArgs(name)
Person.save(name)
mock.verify()
mock.restore()
})
})
從上面的針對(duì)
spy
,stub
,mock
的測(cè)試用例可以很明顯的看出,spy
見名知義,主要是在不改變函數(shù)本身的前提下,收集函數(shù)本身的信息,如:是否被調(diào)用,調(diào)用的參數(shù)等等。
stub
主要將一些有不確定因素的函數(shù)替換掉,保證返回的結(jié)果是你想要的,比如然后根據(jù)不同的返回值來覆蓋不同的語句,基本上網(wǎng)絡(luò)請(qǐng)求呀,數(shù)據(jù)庫呀還有一些耗時(shí)操作等.
mock
這個(gè)詞就很有爭議啦,當(dāng)你才開始寫單元測(cè)試的時(shí)候,遇到一個(gè)函數(shù)中的操作不好寫測(cè)試的時(shí)候,有的前輩可能就會(huì)說把它mock掉啊,然后你就去google,但是可能最后你就只是stub那個(gè)對(duì)象或是函數(shù),就形成了很多人對(duì)mock和stub有點(diǎn)傻傻分不清的,我就是其中一個(gè),啊哈哈哈哈哈。其實(shí)mock來說應(yīng)該謹(jǐn)慎使用,因?yàn)閙ock可能會(huì)使對(duì)象變得很具體,具體就代表著不靈活了,對(duì)于測(cè)試用例來說這是很致命的,適用性大大降低。mock出來的對(duì)象最大的特點(diǎn)就是它自帶斷言,而且不會(huì)真正的走測(cè)試代碼邏輯,然后我們?cè)诖a執(zhí)行后,驗(yàn)證該邏輯是否是我們想要的。
有些話想要講
相對(duì)入門級(jí)測(cè)試玩家來說Jest絕對(duì)是一大福音,環(huán)境配置簡單,直接可以上手。當(dāng)然,當(dāng)你寫的測(cè)試代碼越多,你可能想要測(cè)試得更細(xì)粒度,更全面,再上手Sinon, 是一個(gè)不錯(cuò)的選擇。最后一句有那么一點(diǎn)點(diǎn)營養(yǎng)的話
當(dāng)寫測(cè)試代碼很麻煩的時(shí)候,使用測(cè)試替身,絕對(duì)是不二選擇