Jest基於dva框架的單元測試最佳實踐

yellow超發表於2019-03-04

前言

以前單元測試在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()
  })
})

複製程式碼

從上面的針對spystubmock的測試用例可以很明顯的看出,spy見名知義,主要是在不改變函式本身的前提下,收集函式本身的資訊,如:是否被呼叫,呼叫的引數等等。


stub主要將一些有不確定因素的函式替換掉,保證返回的結果是你想要的,比如然後根據不同的返回值來覆蓋不同的語句,基本上網路請求呀,資料庫呀還有一些耗時操作等.


mock這個詞就很有爭議啦,當你才開始寫單元測試的時候,遇到一個函式中的操作不好寫測試的時候,有的前輩可能就會說把它mock掉啊,然後你就去google,但是可能最後你就只是stub那個物件或是函式,就形成了很多人對mock和stub有點傻傻分不清的,我就是其中一個,啊哈哈哈哈哈。其實mock來說應該謹慎使用,因為mock可能會使物件變得很具體,具體就代表著不靈活了,對於測試用例來說這是很致命的,適用性大大降低。mock出來的物件最大的特點就是它自帶斷言,而且不會真正的走測試程式碼邏輯,然後我們在程式碼執行後,驗證該邏輯是否是我們想要的。

有些話想要講

相對入門級測試玩家來說Jest絕對是一大福音,環境配置簡單,直接可以上手。當然,當你寫的測試程式碼越多,你可能想要測試得更細粒度,更全面,再上手Sinon, 是一個不錯的選擇。最後一句有那麼一點點營養的話

當寫測試程式碼很麻煩的時候,使用測試替身,絕對是不二選擇

相關文章