Jest单元测试进阶
Jest 命令行窗口中的指令
在学习Jest单元测试入门的时候,给Jest命令提供了一个参数 --watchAll, 让它监听测试文件或测试文件引入的文件的变化,从而时时进行测试。但这样做也带来一个问题,只要改变一点内容,Jest就会把所有的测试都跑一遍,有点浪费资源。有没有可能对--watchAll模式进行进一步的优化?在命令窗口中执行npm run test 看一看就知道了, 测试完成后,你会发现还有很多提示(Watch Usage),这些就是对--watchAll模式的优化
Press f to run only failed tests. 按f 键,只测试以前失败的测试。执行npm run test 的时候,发现有一个测试失败了,这时我们只想测试这个失败的测试,可以按f了。演示一下,随便把一个测试用例改为错误,比如 把request 的mock 改为name: 'jason'
jest.mock('request', () => {
return (url, callback) => {
callback(null, 'ok', {name: 'jason'})
}
});
测试重新跑了一遍了(watchAll 模式),命令窗口中显示了错误, 并且在最下面显示press w to show more, 同时光标在闪烁,等待输入。此时按w, 显示了上图中的内容,再按f, 只跑了失败的测试,因为三个测试skipped 了, 当然肯定还是有错误,因为我们还没有修改测试代码
修改测试代码到正确并保存,它只跑了刚才失败的测试。但此时你再修改func.test.js 文件或其它测试用例,发现测试不会再运行了,显示No failed test found,按f键退出
因为f模式是测试以前,就是上一次,失败的测试,我们已经修改好了上一次失败的测试,所以它就不会再进行测试了。按f, 重新回到了watchAll 模式。
总结一下,f 模式的使用就是,npm run test 有失败测试,按f, 修改失败到成功,再按f 退出该模式。
Press o to only run tests related to changed files. 按o ,只会去测试和当前被改变文件相关的测试。但这时,你按o,发现报错了。为什么呢?因为让Jest 去测试改变文件中的测试,但Jest它自己并不知道哪个文件发生了变化,Jest本身,不具备比较文件改变的功能,那怎么办?需要借助git. 因为git 就是追踪文件变化的,只要把工作区和仓库区的代码一对比,就知道哪个文件发生变化了。因此需要把项目变成git 项目。在根目录下,先建.gitignore 文件,再执行git init, 把项目变成git 项目,否则会把node_modules 放到 git 仓库中。为了演示,把fetchData的测试从func.test.js拆分为出来,就叫fetchData.test.js
jest.mock('request', () => {
return (url, callback) => {
callback(null, 'ok', {name: 'sam'})
}
}); const fetchData = require('./func').fetchData; test('should return data when fetchData request success', () => {
return fetchData().then(res => {
expect(res).toEqual({name: 'sam'})
})
})
git add . and git commit -m "init git" 把文件提交到git 仓库。执行npm run test 启动测试,按o 进入到o 模式, 可以看到如下提示,没有文件发生变化。
这时更改一个文件,如forEach 加一个空行,看一下控制台,只有func.test.js 测试文件执行了,其它测试文件并没有执行, 再改一个fetchData.test.js 文件,两个测试文件执行了,还是只跑改变文件中的测试。这时让我想起了jest 命令的另一个参数,--watch, o 模式 不就是--watch 吗。把package.json 中的 --watchAll 改成 --watch
"scripts": {
"test": "jest --watch"
},
重新启动npm run test, 有了-a模式,run all the tests, 这不就是--watchAll, 原来 --watch, --watchAll, a 模式,o 模式,是这样互通的。
再看一下p, 按照文件名执行测试,我们提供一个文件名,它只会对该文件进行测试,可以使用正则表达式来匹配文件名。按p, 提示输入pattern, 再输入fetch, 它就会用fetch 去匹配所有的测试文件名,找到了fetchData.test.js 测试文件,然后它就执行了。如果找不到任何测 试文件,它什么测试都不会执行。
t 则是匹配的test 名字,每一个test 都有一个描述,这个描述可以称之为test 的名字。提供一个test 的名字,它只跑这个test,用法和p 一样。
q 退出watch, enter 就是跑一次单元测试,无论是在什么模式下,只要按enter,就会跑一次对应模式的测试。
Jest 快照测试
传统的单元测试通常都是,执行代码,断言比较,看看是不是和预期的效果一致。但这种断言测试在某些情况下不太合适。比如配置文件,配置文件本来就是放在那里,不用执行代码,如果进行比较断言,就是它和它本身进行比较,肯定相等,这种测试不是太好。之所以对配置文件进行测试,就是希望它不要被随便改了。如果改了,要通知到同事。还有就是UI,它就长这样,不用断言了,我们更希望UI 做好以后,不要随便改了,确定要改了,那就通知大家。对于这两种不希望被改的场景,更好的测试办法,就是把它保存起来,以后每次测试的时候,就和保存的内容进行对比,看有没有改变。把某一时刻的内容保存起来,就是形成了快照,快照就是用来保存的某一时刻的状态。Jest的快照测试也是如此,保存与对比。它提供了一个toMatchSnapshot() 方法,当第一次运行测试的时候,没有快照,那就把这一时刻的状态保存起来,形成快照。以后再运行测试的时候,它会生成一个新的那一时该的快照与以前的快照进行比较,如果两者一致,就表明内容没有变化,测试通过,如果两者不一致了,就表示内容发生改变,jest 就会报错了。新建config.js,
export const axiosConfig = {
devUrl: 'local',
productUrl: '/',
header: {
'accet': 'json'
}
}
config.test.js 写一个快照测试
import { axiosConfig } from './config'; test('axios config', () => {
expect(axiosConfig).toMatchSnapshot();
})
npm run test, 这时项目根目录下生成了一个文件夹__snapshots__,下面有一个config.test.js.snap, 这就是生成的快照文件,它的名字和测试文件的名字一致。打开看一看,它就是把文件内容用字符串的形式保存起来了。这时不管运行多少次npm run test, 测试都会通过,因为config.js 没有改。改变一下,比如加一下id
export const axiosConfig = {
devUrl: 'local',
productUrl: '/',
header: {
'accet': 'json'
},
id: ''
}
这时jest 报错了,如果确定要改成这样,那就需要更新快照了。命令行中按w,多了u和i, u就是表示更新失败快照,i 则表示交互式的更新快照。 此时按u, 测试重跑,更新成功。那什么是交互式的更新快照呢?它是针对多个失败的快照而言的,按i,你可以一个一个进行快照的确认和更新,如果按u,则是一次性全部更新所有快照,可能不是你想要的结果。如果两个快照失败了,一个要更新,一个不要更新,那u就无能为例了,只能使用i。在config.js再写一个配置
export const fetchConfig = {
method: 'post',
time: '2019'
}
那测试文件中,再写一个快照测试
test('fetch config', () => {
expect(fetchConfig).toMatchSnapshot();
})
这时测试肯定没有问题,同时改一下两个配置文件,id改了'24', time改为'2019/11', 2个快照测试都失败了。此时按i, 你会发现只显示一个快照失败,表示这次只确认这一个失败的快照,它也提示了u更新快照,s 跳过这个测试,q退出该交互模式,如果更新,按u,此时又显示了一个失败的快照,再按u,更新完毕。这就是交互式,一个一个的更新,更为灵活。
但有的时候,time是动态生成的,比如fetchConfig中的time 改成new Date(), 每一次跑单元测试的时候,time 都不一样,jest肯定报错。这时可以给toMatchSnapshot() 方法传递一个参数{ time: expect.any(Date) 表示什么时间都可以,就不要匹配时间了。
test('fetch config', () => {
expect(fetchConfig).toMatchSnapshot({
time: expect.any(Date)
});
})
Jest Manual Mock
在以前mock 函数的时候,我们都会把mock 函数的实现放到测试文件中。manual mock 则是创建一个文件夹__mocks__, 把所有mock 函数的实现放到该文件夹下,不过这里要注意 __mocks__ 文件夹的位置,你要mock 哪个文件中的函数,__mocks__文件夹就要和哪个文件放到同一级目录中。新建__mocks__文件夹之后,再在其下面新建一个和要mock的文件的同名文件,在这个文件中就可以写函数的实现。 比如我们要mock func.js 中fetchData, 那就要在func.js 同一级目录(根目录)中新建__mocks__, 然后在其下面建func.js 文件件,在func.js 中就可以mock fetchData。
当在测试文件中,jest.mock(./func.js),然后引入fetchData 时,jest 自动会到__mocks__ 目录中找func.js 文件,取里面的fetchData 函数,这就是mock的函数了。fetchData.test.js
jest.mock('./func');
const fetchData= require('./func').fetchData; test('should return data when fetchData request success', () => {
return fetchData().then(res => {
expect(res).toEqual({name: 'sam'})
})
})
这种mock 有一个问题,在fetchData.test.js 里面测试 一个add.
const add = require('./func').add;
test('add', () => {
let sum = add(, );
expect(sum).toBe()
})
测试报错,add is not a function. 这是因为jest.mock('./func.js'); 整个func.js 模块被mock了,require('./func').add 的时候,它是从mock 的func.js 模块中,就是__mocks__
文件下的func.js 里面去找add, 很显然,没有,所以报错了。怎么解决,不使用require了,使用jest.requireActual(), 字面意思,require真实的,就是从真实的模块,而不是从mock 的模块中引入。 jest.requireActual('./func').add, 从真的func.js中引入add.
const add = jest.requireActual('./func').add;
有mock 就有unmock(), 取消mock。
jest.unmock('./func.js');
如果你mock 的是node_modules 第三方模块,那就要在根目录(node_modules同级目录)新建__mocks__ 文件夹,然后在其下面新建和要mock 模块同名的文件句,如mock request.js 模块,使用request.js 的时候,我们使用的是require('request'), 所以就可以在__mocks__ 文件中建一个request.js.
// 自动mock 这个模块(request) 所有暴露出来的方示
jest.genMockFromModule('request');
let request = require('request');
request = jest.fn((url, fn) => {
fn('error', 'body', {name: 'sam'});
} )
module.exports = request;
当mock的node_modules中的模块时,jest 是自动mock, 执行测试的时候,如果看到你require 第三方模块,它自动会从__mocks__文件夹中找这个模块,肯定是mock的模块。
mock timer
它主要是针对定时器setTimeout, setTimeinterval 提出的。比如在代码中有一个函数需要3s 之后执行,那么在测试的时候,就要在3s以后,测试函数有没有执行,有点浪费时间
function lazy(fn) {
setTimeout(() => {
fn();
}, );
} test('should call fn after 3s', (done) => {
const callback = jest.fn();
lazy(callback);
setTimeout(() => {
expect(callback).toBeCalled();
done()
}, );
})
所以jest 提供了mock timer 的功能,不要再使用真实的时间在这里等了,一个假的时间模拟一下就可以了。首先是jest.useFakeTimers() 的调用,它就告诉jest 在以后的测试中,可以使用假时间。当然只用它还不行,因为它只是表示可以使用,我们还要告诉jest在哪个地方使用,当jest 在测试的时候,到这个地方,它就自动使用假时间。两个函数,jest.runAllTimers(), 它表示把所有时间都跑完。jest.advanceTimer() 快进几秒。具体到我们这个测试,我们希望执完lazy(callback) 就调用, 把lazy函数中的3s时间立刻跑完。可以使用jest.runAllTimers();
jest.useFakeTimers(); // 可以使用假函数 test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
jest.runAllTimers(); // 在这里,把lazy函数里面的3s立即执行完
expect(callback).toBeCalledTimes();
})
但如果我们的lazy 函数中有两个setTimeout 函数,runAllTimers 就会有问题,因为它把所有时间都跑完了,不管有几个setTimeout. 把lazy 函数改为如下
function lazy(fn) {
setTimeout(() => {
fn();
setTimeout(() => {
fn();
}, );
}, );
}
你会发现fn 被调用了两次。但有时,只想测试最外层的setTimeout有没有被调用,这时就要用jest.advanceTimersByTime(3000)
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
jest.advanceTimersByTime(); // 快进3秒
expect(callback).toBeCalledTimes();
})
没有问题,如果再想测试内层的setTimout 有没有被调用,再快进就好了,不过要注意快进的时间,2s, 因为它会在上一个advanceTimerByTime的时间基础上进行快进
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
jest.advanceTimersByTime(); // 快进3秒
expect(callback).toBeCalledTimes();
jest.advanceTimersByTime(); // 再快进2秒
expect(callback).toBeCalledTimes();
})
现在你会发现,如果在一开始的时候,直接快进5s,它的效果就和runAlltimers 一样了。最后一个问题,就是多个测试中都使用advanceTimersByTime,因为它是累加时间的,第二个测试的advanceTimersByTime的时间肯定会在第一个测试中的advanceTimersByTime 时间上相加。解决办法是beforeEach(). 在beforeEach 中调用jest.useFackTimers,每次测试之前,先初始化timer,把timer归零
beforeEach(() => {
jest.useFakeTimers(); // 可以使用假函数
}) test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
jest.advanceTimersByTime(); // 快进3秒
expect(callback).toBeCalledTimes();
jest.advanceTimersByTime(); // 再快进2秒
expect(callback).toBeCalledTimes();
}) test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
jest.advanceTimersByTime(); // 快进3秒
expect(callback).toBeCalledTimes();
jest.advanceTimersByTime(); // 再快进2秒
expect(callback).toBeCalledTimes();
})
Jest单元测试进阶的更多相关文章
- Jest 单元测试入门
今天,我们要讲的是 Jest 单元测试的入门知识. 为何要进行单元测试? 在学习 Jest 之前,我们需要回答一个问题:为何要进行单元测试?编写单元测试可以给你带来很多好处: 将测试自动化,无需每次都 ...
- 【SpringBoot】单元测试进阶实战、自定义异常处理、t部署war项目到tomcat9和启动原理讲解
========================4.Springboot2.0单元测试进阶实战和自定义异常处理 ============================== 1.@SpringBoot ...
- 【vue】在VS Code中调试Jest单元测试
在VS Code中调试Jest单元测试 添加调试任务 打开 vscode launch.json 文件,在 configurations 内加入下面代码 "configurations&qu ...
- vue2项目,踩坑Jest单元测试
目前的项目已经维护了挺久,由于客户要求,我们要为项目加上单元测试,挑选一番后选择了Jest(配置简便,开箱即用),下面记录了此次为项目添加Jest作为单元测试的经历. 安装Jest 1. 在项目目录下 ...
- go单元测试进阶篇
作者介绍:熊训德(英文名:Sundy),16年毕业于四川大学大学并加入腾讯.目前在腾讯云从事hadoop生态相关的云存储和计算等后台开发,喜欢并专注于研究大数据.虚拟化和人工智能等相关技术. 本文档说 ...
- 小D课堂 - 零基础入门SpringBoot2.X到实战_第4节 Springboot2.0单元测试进阶实战和自定义异常处理_17、SpringBootTest单元测试实战
笔记 1.@SpringBootTest单元测试实战 简介:讲解SpringBoot的单元测试 1.引入相关依赖 <!--springboot程 ...
- 2020,你需掌握go 单元测试进阶篇
本文说明go语言自带的测试框架未提供或者未方便地提供的测试方案,主要是用于解决写单元测试中比较头痛的依赖问题.也就是伪造模式,经典的伪造模式有桩对象(stub),模拟对象(mock)和伪对象(fake ...
- [Web 测试] Jest单元测试的几个指标
三个参数代表什么? %stmts是语句覆盖率(statement coverage):是不是每个语句都执行了? %Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了 ...
- antd-pro1.0使用jest对react组件进行单元测试
前言 基于React+Ant Design(以下用Antd表示)的项目,在对于自己封装的,或者基于Antd封装的公共组件的自动化测试技术的选型和实践. 背景 随着前端项目越来越大,业务逻辑日益繁杂,协 ...
随机推荐
- views视图
1.request.POST.get('.......') --radio 单选框 get()方法 从HTML中提取发过来的数据 1. 2. 3. 4. 2.request.POS ...
- c语言定义的几种易错的说明
int p; //一个整数 int p [5]; //一个包含5个整数的数组 int * p; //指向整数的指针 int * p [10]; //一个包含10个整数指针的数组 int ** p; / ...
- 常用dos命令(3)
网络命令 ping 进行网络连接测试.名称解析 ftp 文件传输 net 网络命令集及用户管理 telnet 远程登陆 ipconfig显示.修改TCP/IP设置 msg 给用户发送消息 arp 显示 ...
- js日志组件封装
js日志组件~~ 1 function Logger(level) { if (!(this instanceof Logger)) { return new Logger(); } var ERRO ...
- Jupyter-notebook安装问题及解决
两种方式: 1.pip install jupyter notebook 2.安装Anaconda 1.pip安装 通过命令行pip,要注意是在哪个虚拟环境,安装好后jupyter notebook所 ...
- ESA2GJK1DH1K基础篇: Android连接MQTT简单的Demo
题外话 我老爸也问我物联网发展的趋势是什么!!!!!! 我自己感觉的:(正在朝着 "我,机器人" 这部电影的服务器方向发展) 以后的设备都会和服务器交互,就是说本地不再做处理,全部 ...
- sublime插件开发: 文件说明
sublime插件开发 文件 .sublime-settings 设置文件 Main.sublime-menu 主菜单按钮配置文件 Side Bar.sublime-menu 侧边栏菜单文件列表,选中 ...
- 只访问tomcat,不访问项目时,显示指定内容。
1.情景展示 我们知道,将javaWeb项目部署到tomcat后,访问该项目的url路径构成是: 网路协议+"://"+ip地址+":"+tomcat设定的 ...
- WebGPU学习(七):学习“twoCubes”和“instancedCube”示例
大家好,本文学习Chrome->webgpu-samplers->twoCubes和instancedCube示例. 这两个示例都与"rotatingCube"示例差不 ...
- spark 读取 ftp
class FtpShow(spark: SparkSession, map: Map[String, String]) { private val path = map(FtpOptions.PAT ...