Jest基于dva框架的單元測(cè)試最佳實(shí)踐

前言

以前單元測(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ì)spystubmock的測(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ì)是不二選擇

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容