用Jest单元测试维护你的javascript代码
单元测试的应用场景
笔者不太推荐所有业务都使用单元测试
但是一些JS公共组件库, 需要用单元测试来保证其模块API的稳定性
安装
npm install --save-dev jest babel-jest babel-core
编辑package.json, 添加scripts: { test: jest }
npm t即可
常用技巧
模拟ajax返回
场景
代码情况: 业务调用ajax, 根据返回内容做处理数据
let examples = {
data: 0,
addData: () => {
$.ajax({
...
success: function(result){ examples.data += result}
})
}
}
测试用例
it('检查处理数据', () => {
let $ = require('jquery'),
examples = require('../examples.js')
$.ajax = jest.fn()
examples.addData()
$.ajax.mock.calls[0][0].success(1)
expect(examples.data).toBe(1)
})
重点在于$.ajax = jest.genMockFunction() 表示以后$.ajax都会被模拟处理
$.ajax.mock.calls[0][0].success表示的是: $.ajax中第1次被调用时的第一个参数中success变量
整个逻辑就是模拟ajax会返回数字1, 然后验证其最终的加法是否正确
简单定时器处理
场景
要等待定时器执行完毕后, 然后验证其计算结果
let examples = {
data: 0,
addData: () => {
setTimeout(() => {
examples.data += 1
}, 3000)
}
}
测试用例
it('测试定时器' => {
jest.useFakeTimers()
let exampels = require('../examples.js')
examples.addData()
setTimeout.mock.calls[0][0]()
expect(examples.data).toBe(1)
})
jest.useFakeTimers()的作用是对(setTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate and clearImmediate等函数进行mock
作用类似于setTimeout = jest.fn()
方案2: 模拟运行时间
setTimeout.mock.calls[0][0]()替换成jest.runTimersToTime(3000)
也可以通过测试, runTimersToTime(3000)表示时间向前运行了3000毫秒
方案3: 运行等待的时间类任务
setTimeout.mock.calls[0][0]()替换成
jest.runAllTimers()或
jest.runOnlyPendingTimers() 也会完成当前所有等待的时间类任务
这两者的区别后面会谈到
异步
场景一: callback回调
例如JSONP, 大部分情况下我们都mock掉
但是假如我们真的要测试jsonp方法是否有把请求发出去, 并验证接口的返回
测试用例一: done方法
it('测试真实ajax', done => {
$.ajax({
url: 'http://dynamic.vip.*.com/cashv2/userCashList',
data: {
paybiz: 'supervip'
},
timeout: 2000,
success: result => {
expect(result).toEqual({ result: -1, msg: '登录验证失败!'});
done()
},
error: result => {}
})
})
只有真正运行到done, 单元测试才会结束
要注意假如expect不正确, 就不会执行下面的done 并且不会告诉expect前后对比差在哪里, 只会等到jest的timeout错误
测试用例二: Promise方法
it('测试真实ajax', () => {
expect.assertions(1)
return $.ajax({
url: 'http://dynamic.vip.*.com/cashv2/userCashList',
data: {
paybiz: 'supervip'
},
timeout: 2000
})
.then(result => expect(result).toEqual({ result: -1, msg: '登录验证失败!'});
)
})
expect.assertions(1)表示要执行一次expect校验
测试用例三: .resolves / .rejects
it('测试真实ajax', () => {
expect.assertions(1)
return $.ajax({
url: 'http://dynamic.vip.*.com/cashv2/userCashList',
data: {
paybiz: 'supervip'
},
timeout: 2000
})
.resolves()
.toEqual({ result: -1, msg: '登录验证失败!'});
)
})
修改userAgent
userAgent代表一些不能被JS修改的变量
有时我们的业务代码对不同的userAgent做判断
那单元测试应该怎跑
场景
let utils = {
isWeixin: function() {
let ua = navigator.userAgent.toLowerCase();
return ua.match(/MicroMessenger/i) == "micromessenger")
}
}
测试用例
it('测试isWeixin', () => {
let util = require('../../src/common/util.js').default
Object.defineProperty(navigator, 'userAgent', {
writable: true,
value: ' MicroMessenger '
})
expect(util.isWeixin()).toBe(true)
})
测试模块内部不公开的函数
场景
有时候有些模块里的内部方法不想被外部调用, 而不暴露在模块里
这时候我们应该如何进行测试
测试用例
这里我们主要用rewire这个库
let rewire = require('rewire'),
path = require('path')
//getHtmlUrl即为我们没暴露, 但是要进行测试的方法
it('test', () => {
//getHtmlUrl就是../../src/ci模块中的内部方法
let app = rewire(path.join(__dirname, '../../src/ci')),
getHtmlUrl = app.__get__('getHtmlUrl')
expect(getHtmlUrl()).toEqual('xxx')
})
这里有个坑就是 ../../src/ci如果直接用require的时候是可以的
但是rewire的时候就说找不到模块, 所以采用了绝对路径
Mock整个第三方库
场景
let { exec } = require('child_process'),
ci = {
check: () => {
exec('git log --name-only -1', (error, stdout, stderr) => {
...
}
}
}
测试用例
我们需要测试的是 回调中的处理
但是exec里执行的我们要mock掉, 而且回调中的参数 要是我们mock的
//在__tests__同父目录下新建, __mocks__/child_process.js
const child_process = jest.genMockFromModule('child_process');
let exec_callback_args = []
function __setMockExec(args = []) {
exec_callback_args = args
}
function exec(_, callback) {
callback && callback.apply(null, exec_callback_args )
}
child_process.__setMockExec = __setMockExec;
child_process.exec = exec;
module.exports = child_process;
//test.js
jest.mock('child_process')
it('测试处理exec', () => {
let myStdout = 'ahah',
myStderr = 'aaa',
ci = require('../src/ci.js')
require('child_process').__setMockExec([null, myStdout, myStderr])
ci.check()
expect...
})
这种方式是写mock的时候会比较麻烦
但是写好一次后 以后类似的mock功能 就可以通用了
场景: 简单mock自己的库
业务代码:
不需要测试的库文件
let msgbox = function(){...}
module.exports = msgbox
依赖了不需要测试的库文件
let utils = {
check: (result) => {
if ( result === -100 ){
msgbox()
}
}
}
而我们 需要测试 check 方法
测试用例
jest.mock('../../src/common/msgbox.js', () => {
return jest.fn()
})
beforeEach(() => {
msgbox.mockClear()
})
it('-100要重新登录时 ', () => {
let result = util.checkAjaxError({
result: -100
})
expect(msgbox.mock.calls[0][0]).toBe('抱歉,您当前的登录信息异常,需要重新登录。')
})
mock 引用的css文件、html和tpl文件
在引用HTML时, jest一般都会报错
例如
import tplItem from './item.html';
在执行jest测试时候 会报错
/packages/vip-vconsole/src/log/item.html:1
({"Object.":function(module,exports,require,__dirname,__filename,global,jest){