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

前言

以前单元测试在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": "/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, 是一个不错的选择。最后一句有那么一点点营养的话

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

你可能感兴趣的:(Jest基于dva框架的单元测试最佳实践)