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

发表于:2017-12-29 11:49

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:1478590378104    来源:稀土掘金

  前言
  以前单元测试在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),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号