前言
以前单元测试在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, 是一个不错的选择。最后一句有那么一点点营养的话
当写测试代码很麻烦的时候,使用测试替身,绝对是不二选择
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。