前言
以前單元測試在JavaScript專案中配置其實還是挺繁瑣的,依賴各種庫mocha,chai,sion或者第三方覆蓋率報表生成庫,但是現在Facebook推出了Jest測試框架,並在react native專案初始化時就已經整合了該環境,所以還沒玩過的同學們可以耐心的看下去,說不定玩一次就愛上了寫單元測試呢。
Jest框架
Jest已經內建了斷言,mock方案,以及非同步處理(async/await),只需簡單配置即可匯出程式碼覆蓋率報告,還有針對於UI的快照測試。官方聲稱的
Delightful JavaScript Testing
?
環境配置
因為是基於dva框架開發的react native專案,所以我們著重測試model類的方法(reducers和effects)
- package.json中針對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
是否開啟跑測試程式碼時收集覆蓋率
coverageReporters
匯出報告檔案型別(通過該匯出的檔案和上傳到sonar分析)
transformIgnorePatterns
transformIgnorePatterns
將一些model中涉及到的npm進行babel轉換,不然在測試中無法識別es6的語法
moduleNameMapper
指定需要mock庫對應的mock檔案
如何寫一個測試程式碼
首先,介紹下這個model的reducr和effect方法的功能(具體dva的model怎麼寫,可以github下,這裡不多篇幅講解)。reducers中的changeLoginStatus很簡單就是根據payload的物件改變state中對應的key;而effects中的login方法(注:這是一個generator)就是根據請求體payload中的引數進行網路請求,這裡我已經封裝成一個方法了,根據返回的response來呼叫對應的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)
}
}
}
}
複製程式碼
主要就是測試reducer和effect方法
login-test.js
describe(`LoginModel------------>reducer`, () => {
it(`changeLoginStatus -> state all key should change to setvalue`, () => {
// reduce 引數1:state初始值;引數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)
這是呼叫一個NativeModule方法,在執行測試的時候,你可能會發現會報找不到YCUserInfoPlugin的setUserToken方法,各位看官不急,因為這個是寫在native的,我們也不需要關係它是否正確,只需知道呼叫了這句話即可,我們可以把它mock掉。怎麼做能?
- 方法一:可以直接在當前測試檔案,在import前執行如下程式碼:
jest.mock(`react-native`, () => {
NativeModule: {
YCUserInfoPlugin: {
setUserToken: () => {}
}
}
})
import ...
import ...
code
複製程式碼
- 方法二:在建立一個名為mocks的資料夾,因為需要mock的react-native包中NativeModule物件中的YCUserInfoPlugin,所以建立建立檔案為react-native.js,然後在package.json的moduleNameMapper中配置改檔案的路徑,即
包名: `檔案所在的路徑`
mocks/react-native.js
export default const NativeModules = {
YCUserInfoPlugin: {
setUserToken: () => {}
}
}
複製程式碼
這樣jest就知道在跑測試程式碼時,去找我們mock的檔案了,test case 也可以順利跑過了。因為這個測試用例中只需要知道那句程式碼執行就ok啦。
測試程式碼解析
在執行單個測試用例的時候,有可能會遇到全域性設定的問題,你可以在beforeAll()或是在afterAll()週期方法中做一些初始化和回滾現場的操作。
一般來說我們主要測試資料互動的模組,所以model就是重點,正常來說我們網路請求這塊是需要mock掉的,但是因為在dva框架中,我們一般把網路請求封裝在effects中,而且這個方法是個generator函式(dva框架整合的redux-saga),我們可以很方面的在裡面的每一個yeild語句裡自定義返回值,就可以設定不同型別的返回值,來執行不同的語句覆蓋。
使用體驗吐槽
jest中針對於測試替身這塊的能力還是沒有Sinon厲害,而且API又少,文件有誤導 性,想要更深入的寫一些測試用例還得藉助第三方的包。
Sinon介紹
當你在寫測試程式碼中不順利的時候,或是把其中的程式碼變為測試替身,絕對是一個不二選擇。下面可以看下簡單的測試用例,來了解下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()
})
})
複製程式碼
從上面的針對
spy
,stub
,mock
的測試用例可以很明顯的看出,spy
見名知義,主要是在不改變函式本身的前提下,收集函式本身的資訊,如:是否被呼叫,呼叫的引數等等。
stub
主要將一些有不確定因素的函式替換掉,保證返回的結果是你想要的,比如然後根據不同的返回值來覆蓋不同的語句,基本上網路請求呀,資料庫呀還有一些耗時操作等.
mock
這個詞就很有爭議啦,當你才開始寫單元測試的時候,遇到一個函式中的操作不好寫測試的時候,有的前輩可能就會說把它mock掉啊,然後你就去google,但是可能最後你就只是stub那個物件或是函式,就形成了很多人對mock和stub有點傻傻分不清的,我就是其中一個,啊哈哈哈哈哈。其實mock來說應該謹慎使用,因為mock可能會使物件變得很具體,具體就代表著不靈活了,對於測試用例來說這是很致命的,適用性大大降低。mock出來的物件最大的特點就是它自帶斷言,而且不會真正的走測試程式碼邏輯,然後我們在程式碼執行後,驗證該邏輯是否是我們想要的。
有些話想要講
相對入門級測試玩家來說Jest絕對是一大福音,環境配置簡單,直接可以上手。當然,當你寫的測試程式碼越多,你可能想要測試得更細粒度,更全面,再上手Sinon, 是一個不錯的選擇。最後一句有那麼一點點營養的話
當寫測試程式碼很麻煩的時候,使用測試替身,絕對是不二選擇