jest测试html2text,用Jest单元测试维护你的javascript代码

用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){

SyntaxError: Unexpected token <

当这些HTML对我们的测试用例无影响时候 可以这样mock

引入identity-obj-proxy模块

npm install --save-dev identity-obj-proxy

tips:

identity-obj-proxy主要的作用就是更改import模块的时候 其导出的是一个无论key为何值 value都等于key的一个对象

在项目跟目录创建jest.config.js文件

moduleNameMapper: {

"\\.(html)$": "identity-obj-proxy"

}

alias查找路径重命名

例如像笔者一般习惯在webpack.config.js里配置

{

...

resolve: {

alias: {

"@": path.join(__dirname, 'src', 'js')

}

}

}

业务JS

import xxx from '@/pages/abs'

然后在跑业务JS的时候 jest并找不到 '@/pages/abs'

而这时 jest配置里也有webpack类似的alias功能

{

...

"jest": {

"^@/(.*)$": "/src/js/$1"

}

}

这样配置就可以设置正则匹配到路径 如何重新找路径

监听location.href的赋值

场景: 要测试代码有无执行更改url

例如业务代码

location.href = 'https://xxx.com'

测试代码就为

global.window = Object.create(window)

Object.defineProperty(window, 'location', {

value: {

href: 'https://www.aaa.com'

}

})

...

expect(window.location.href).toMathSnapshot(`https://xxx.com`)

new Date的mock

有时候断言含有时间戳, 应该如何mock

const DATE_TO_USE = new Date('2016');

const _Date = Date;

global.Date = jest.fn(() => DATE_TO_USE);

global.Date.UTC = _Date.UTC;

global.Date.parse = _Date.parse;

global.Date.now = _Date.now;

疑惑点

jest.runAllTimers()和jest.runOnlyPendingTimers()的区别

jest.runAllTimers(): 为执行当前所有的时间类任务

jest.runOnlyPendingTimers(): 为执行当前正在等待的所有时间类任务

例子

let infiniteTimerGame = () => {

setTimeout(() => {

console.log('Times up! 10 seconds before the next game starts...');

callback && callback();

// Schedule the next game in 10 seconds

setTimeout(() => {

infiniteTimerGame(callback);

}, 10000);

}, 1000);

}

infiniteTimerGame()

这个任务是

调用方法后等待1000ms后执行回调

过10000ms后就继续第一个步骤

这种情况下 测试代码

执行jest.runOnlyPendingTimers() 只会执行第一个1000ms的正在等待任务

但是jest.runAllTimers() 会执行定时器里的定时任务, 假如定时器里还有定时器, 又会继续执行定时器任务, 这里的例子, 测试用例将会不断的死循环执行

注意事项

resetModules

最好都在单元测试中顶部增加

beforeEach(() => jest.resetModules());

这会在运行每次单元测试前 重置模块

不然全局中的变量的.mock.calls很容易会记录之前单元测试的记录

也可以防止某个单元测试修改了模块, 而影响到了其他单元测试

单元测试覆盖率

> jest --coverage

Test Suites: 2 passed, 2 total

Tests: 18 passed, 18 total

Snapshots: 0 total

Time: 0.695s, estimated 1s

Ran all test suites.

----------|----------|----------|----------|----------|----------------|

File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |

----------|----------|----------|----------|----------|----------------|

All files | 100 | 100 | 100 | 100 | |

index.js | 100 | 100 | 100 | 100 | |

utils.js | 100 | 100 | 100 | 100 | |

----------|----------|----------|----------|----------|----------------|Stmts: 语句覆盖率

Branch: 分支覆盖率

Funcs: 函数覆盖率

Lines: 行覆盖率

其实这些指标都是一个提示, 并不要太在意

通常80%以上的覆盖率就足够

若每个分支都要走一遍 很多是无意义的

什么叫做无意义, 是即使你走了这个分支 你的断言也不知道该怎么检查

这4个指标里 一般百分比最高的是Funcs, 最低的是Branch

我的建议是: 对外开放的接口多测试

而内部使用的方法 保证内部使用正确即可

有什么没讲

ui类测试

react测试

jest其实基本是为react定制的, 但是笔者不太喜欢给UI写测试, 所以这两类测试都木有讲

参考资料

© 404mzk all right reserved,powered by Gitbookhttp://blog.404mzk.com 该文件修订时间:

2020-02-09 21:20:14

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值