Jest基于dva框架的单元测试最佳实践

前言

之前单元测试在JavaScript项目中配置其实仍是挺繁琐的,依赖各类库mocha,chai,sion或者第三方覆盖率报表生成库,可是如今Facebook推出了Jest测试框架,并在react native项目初始化时就已经集成了该环境,因此还没玩过的同窗们能够耐心的看下去,说不定玩一次就爱上了写单元测试呢。javascript

Jest框架

Jest已经内置了断言,mock方案,以及异步处理(async/await),只需简单配置便可导出代码覆盖率报告,还有针对于UI的快照测试。官方声称的Delightful JavaScript Testing 😁java

环境配置

由于是基于dva框架开发的react native项目,因此咱们着重测试model类的方法(reducers和effects)node

  • 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 是否开启跑测试代码时收集覆盖率react

coverageReporters 导出报告文件类型(经过该导出的文件和上传到sonar分析) transformIgnorePatternsgit

transformIgnorePatterns 将一些model中涉及到的npm进行babel转换,否则在测试中没法识别es6的语法es6

moduleNameMapper 指定须要mock库对应的mock文件github

如何写一个测试代码

首先,介绍下这个model的reducr和effect方法的功能(具体dva的model怎么写,能够github下,这里很少篇幅讲解)。reducers中的changeLoginStatus很简单就是根据payload的对象改变state中对应的key;而effects中的login方法(注:这是一个generator)就是根据请求体payload中的参数进行网络请求,这里我已经封装成一个方法了,根据返回的response来调用对应的action,从而改变state。数据库

login.jsnpm

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方法json

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, 是一个不错的选择。最后一句有那么一点点养分的话

当写测试代码很麻烦的时候,使用测试替身,绝对是不二选择

相关文章
相关标签/搜索