一 前端自动化测试产生的背景及原理
在没有前端自动化测试的时候,一般是项目使用过程中发现问题。
前端自动化测试:是写了一段测试的js代码,通过测试的js代码,去运行项目(含有需要测试的代码),查看预期值跟结果的值,是否相等,相等则正确,否则有误。
简单的理解就是 一段额外的测试代码就可以在上线之前对它进行测试,而这些测试不是人肉的去点击,而是通过已经写好的代码去运行的,得到结果进行判断,是否有问题
如 小案例:要测试math.js的方法
math.js
function add(a, b) {
return a - b;
}
function del(a, b) {
return a - b;
}
math.test.js 测试
/**
* expect 方法 是输出错误
* result 结果值
* actual 预期值
*/
function expect(result) {
return {
toBe: function (actual) {
if (result !== actual) {
throw new Error(`预期值${actual}和结果值${result}不相等`);
}
}
}
}
/**
*
* @param {*} desc 描述语
* @param {*} fn 执行测试的方法
*/
function test(desc, fn) {
try {
fn();
console.log(`${desc} 通过测试`)
} catch (e) {
console.log(`${desc} 没有通过测试,${e}`)
}
}
test('测试加法3+7:', () => {
expect(add(3, 7)).toBe(10);
})
test('测试减法法7-7:', () => {
expect(del(7, 7)).toBe(0);
})
二. 前端自动化测试框架 Jest
前端自动化测试框架 主流: Jasmine、 Mocha+chai、 Jest
这些 功能,性能,易用性都很好
Jest 是一个令人愉快的 JavaScript 测试框架,专注于 简洁明快。
Jest是 Facebook 的一套开源的 JavaScript 测试框架, 它自动集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架。并且它对同样是 Facebook 的开源前端框架 React 的测试十分友好。
jest 优点:
1.速度快
比如有2个A,B模块,第一次运行项目A,B同时执行到;第二次运行项目前,改了A,这时B是就不会在运行的,只会运行A,因为他知道B没修改,所以没必须要运行,这样的话,测试效率很高,速度很快
2.API 简单
3.易配置
安装下jest ,简单配置下,就可以使用
4.隔离性好
jest 里面的很多测试文件,但是每个文件被执行的时候环境是隔离的
5.监控模式
这种模式下可以更灵活的
6.IDE整合
7.Snapshot
8.多项目并行
9.覆盖率
10.Mock丰富
三. Jest 使用
1.安装Jest
在项目里执行
npm install jest -D
2. 运行测试
在 package.json
中
"scripts": {
"test": "jest"
},
执行
npm run test
即可启动jest
对项目中所有以.test.js
结尾的文件进行测试。
自动监控测试文件
让jest
自动监控测试文件,一有更新,就自动运行测试。在package.json
中的jest
那里加上--watchAll
参数
"script": {
"test": "jest --watchAll"
}
四 Jest 简单配置
(1)生成配置文件jest.config.js
使用jest
,会使用默认的配置,如果想修改配置,执行下面的命令,来初始化生成一个jest.config.js
文件
npx jest --init
默认配置了,
(2)生成测试覆盖率报告
如果想修改测试覆盖率报告的文件夹名称,可以在jest.config.js中配置,
修改这一项 coverageDirectory: "coverage",
执行命令 npx jest --coverage
在项目目录下会生成一个文件夹,存放测试覆盖率的文件
// coverageDirectory: "coverage",
// 也可以修改为其他名字
coverageDirectory: "reports",
这样我们在运行
npx jest --coverage
或者将package.json
里面增加
"scripts": {
"coverage": "jest --coverage"
}
这样就可以直接
npm run coverage
生成测试覆盖率报告
打开lcov-report/index.html 就可以看测试覆盖率报告
(3)配置可以用esModule模块导入的测试环境
我们运行Jest的时候 当前坏境是一个node环境【node 不支持import ,nodejs采用的是CommonJS的模块化规范,使用require引入模块;而import是ES6的模块化规范关键字】,Jest在node环境下对于esModule的语法无法解析,只辨识commonJS的模块语法
esModule 写法:
// math.js
export function add(a, b) {
return a + b
}
// math.test.js
import {add} from './math'
实际项目中更多使用的是ES
的语法来定义module
,但是如果我们直接改成了ES
语法,则运行jest
就报错了。如何做兼容呢,我们可以使用babel【
必须引入babel转义支持,通过babel进行编译,使其变成node的模块化代码。】
安装 babel
npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
配置babel
在项目根目录新建.babelrc
文件,并写入以下内容
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
配置完毕,再运行npm run test,就不会报错
这个过程:
- 在运行npm run test之后
jest
内部集成一个插件babel-jest
- 它会检测当前项目是否安装了
babel-core
- 如果安装了则会去读取
.babelrc
配置 - 在运行测试之前,会根据babel配置对代码进行转换
- 最后,运行转化后的测试用例代码
五 Jest 中的匹配器
有关完整列表,请参阅expectAPI doc。下面是简单的介绍
基础语法
expect()
返回被称作“expectation”的对象。toBe()
被称作matcher。Jest会对两者进行比较并输出测试结果。
相应的,expect()
和toBe()
还可以调用被测的function
//1. toBe 相当于object.js 或者是===,比较的是内存地址
test('测试1与1相匹配:', () => {
expect(1).toBe(1);
})
//2. toEqual 匹配是内容相等
test('测试对象内容相等:', () => {
const a = { one: 1 }
expect(a).toEqual({ one: 1 })
})
比较null,undefined,true,false
//3. toBeNull 匹配是否为null
test('测试是否为null:', () => {
const a = null;
expect(a).toBeNull()
})
//4. toBeUndefined 真假有关的匹配
test('测试是否为undefined:', () => {
const a = undefined;
expect(a).toBeUndefined()
})
//5. toBeDefined 变量是被定义的
test('测试是否为定义过:', () => {
const a = null; //a = undefined; 的时候是表示没被定义
expect(a).toBeDefined()
})
//6. toBeTruthy 是否为真的 true
test('测试是否为BeTruthy:', () => {
const a = 1; // a 为null ,0,undefined 都是被认为是false,
expect(a).toBeTruthy()
})
//7. toBeFalsy 是否为假 false
test('测试是否为BeTruthy:', () => {
const a = null; // a 为null ,0,undefined 都是被认为是false,
expect(a).toBeFalsy()
})
not
// 8. not 就是否的意思
test('测试是否为BeTruthy:', () => {
const a = null; // a 为null ,0,undefined 都是被认为是false,
expect(a).toBeFalsy()
expect(a).not.toBeTruthy()
})
toBe():使用Object.is()实现精确匹配
toEqual():递归检查对象或数组的每一个字段。
toBeNull():判断是否为null
toBeUndefined():判断是否是undefined
toBeDefined():判断是否是定义过的
toBeTruthy():判断是否为真
toBeFalsy():判断是否为假
not:取反匹配器 expect(a).not.toBeFalsy()
数字相关匹配器
1. toBeGreaterThan 是否大于
2. toBeLessThan 匹配是否小于
3. toBeGreaterThanOrEqual
4. toBeLessThanOrEqual
5. toBeCloseTo 特别用来处理浮点数
test('测试浮点数:', () => {
const a = 0.1;
const b = 0.1;
expect(a + b).toBeCloseTo(0.2);
})
字符串string
使用toMatch(obj) 方法 obj 可以是正则表达式,也可以是字符串
test('toMatch', () => {
const str = "http://www.baidu.com";
expect(str).toMatch(/bai/);
expect(str).not.toMatch(/bai-du/);
});
数组Array
使用toContain()方法:判断数组是否包含某项
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];
test('the shopping list', () => {
const arr = new Set(shoppingList);
expect(shoppingList).toContain('beer');
expect(arr).toContain('beer');
expect(shoppingList).not.toContain('pork');
});
异常情况下
toThrow - 要测试的特定函数会在调用时抛出一个错误 。toThrow 也是可以写正则表达式,和字符串
//throwNewErrorFunc 抛出的错误内容要跟toThrow里的相等
const throwNewErrorFunc = () => {
throw new Error('this is a new error')
}
test('异常情况下', () => {
expect(throwNewErrorFunc).toThrow('this is a new odl error')
});
其他
- .resolves 和 .rejects - 用来测试 promise
用来测试 promise
//resolves
test('resolves to lemon', () => {
// make sure to add a return statement
return expect(Promise.resolve('lemon')).resolves.toBe('lemon');
});
//rejects
test('resolves to lemon', async () => {
await expect(Promise.resolve('lemon')).resolves.toBe('lemon');
await expect(Promise.resolve('lemon')).resolves.not.toBe('octopus');
});
- .toHaveBeenCalled() - 用来判断一个函数是否被调用过
.toHaveBeenCalled() 也有个别名是.toBeCalled(),用来判断一个函数是否被调用过。
describe('drinkAll', () => {
test('drinks something lemon-flavored', () => {
const drink = jest.fn();
drinkAll(drink, 'lemon');
expect(drink).toHaveBeenCalled();
});
test('does not drink something octopus-flavored', () => {
const drink = jest.fn();
drinkAll(drink, 'octopus');
expect(drink).not.toHaveBeenCalled();
});
});
- .toHaveBeenCalledTimes(number) - 判断函数被调用过几次
和 toHaveBeenCalled 类似,判断函数被调用过几次。
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenCalledTimes(2);
});
六 Jest 的命令行参数
f
,只重新运行上次测试失败的测试用例。
o
,有多个测试用例文件,当某个测试文件修改,则只重新运行修改的那个文件里的所有测试。 需要配合使用git管理项目(不然jest 不知道哪个文件修改)
p
,只重新运行测试文件名字跟输入的匹配的测试文件。
t
,只重新运行测试用例名字跟输入的匹配的测试用例。
在 package.json
中
"scripts": {
"test": "jest --watchAll"
},
"test": "jest --watchAll",相当于a模式,当任何一个测试文件修改,则重新运行所有的测试
"test": "jest --watch" ,相当于o模式,
但我们运行的jest
命令行带了--watchAll
或--watch
,执行命令后,终端中该命令并不会退出,而是在等待状态,这时候你可以输入上述中的各种模式的代号,进行重新运行测试
七 异步代码的测试
1.回调函数型型的异步函数
创建fetchData.js
import axios from 'axios';
export const fetchData = (fun) => {
axios.get('http://www.dell-lee.com/react/api/demo.json').then((res) => {
fun(res.data)
})
}
测试用例 fetchData.test.js: 安装之前学的内容,我们上来就写如下测试代码
test('异步函数', () => {
fetchData((data)=> {
expect(data).toEqual({success: true})
})
})
运行,测试通过。
但是!但是!这是错误的写法,因为test
刷就执行完了,expect
压根就没运行,也就是说哪怕要测试的函数返回值并不是{success: true}
,这里也会测试通过的,咋整?稍微修改一下
import { fetchData } from './fetchData'
// 回调函数型型的异步函数
test('异步函数', (done) => {
fetchData((data) => {
expect(data).toEqual({ success: true });
done();
})
})
给test
的回调函数传个参数(函数) ,告诉他,这个传入的函数执行完了,你这个test
才算执行完了。这样就可以等到expect
执行之后,才算这个测试用例完事。
2.返回Promise 型的异步函数
创建fetchData.js
import axios from 'axios';
// 返回Promise 型的异步函数
export const fetchDataPromise = () => {
return axios.get('http://www.dell-lee.com/react/api/demo1.json');
}
方法1:Promise
如果测试正常情况,则
test('返回Promise异步函数:', () => {
return fetchDataPromise((res) => {
console.log(res, 'res :::')
expect(res.data).toEqual({ success: true })
})
})
如果要测试抛出异常的Promise的话,我们觉得应该这么写
test('返回结果为 404', () => {
expect.assetions(1) // 至少要执行一个 expect
return fetchDataPromise().catch(e => {
expect(e.toString().indexOf('404') > -1).toBe(true)
})
})
注意: 如果不加expect.assetions(1) 时,如果该函数正常返回了{success: true},测试结果竟然还是通过!因为catch那里根本没有执行。加上expect.assetions(1) 时,如果expect执行次数不够1,在这里也就是说catch那里没有被执行,就测试失败。
方法2 :.resolves/.rejects
可以使用./resolves匹配器匹配你的期望的声明(跟Promise类似),如果想要被拒绝,可以使用.rejects
如果测试正常情况,则
test('返回Promise异步函数:', () => {
return expect(fetchDataPromise()).resolves.toMatchObject({
data: {
success: true
}
})
})
如果要测试抛出异常的Promise的话,我们觉得应该这么写
test('返回结果为 404', () => {
// toThrow() 抛出异常
return expect(fetchDataPromise()).rejects.toThrow()
})
方法3:Async/Await
若要编写async测试,只要在函数前面使用async关键字传递到test。
// 测试正常情况
test('promise', async () => {
const res = await fetchDataPromise()
expect(res.data).toEqual({ success: true })
})
// 测试异常情况
test('返回 404', async () => {
expect.assertions(1)
try {
await fetchDataPromise()
} catch (e) {
expect(e.toString()).toEqual('Error: Request failed with status code 404')
}
})
方法4: 可以async
与await
和.resolves
或结合使用.rejects
// 如果测试正常情况,则
test('返回Promise异步函数:', async () => {
await expect(fetchDataPromise()).resolves.toMatchObject({
data: {
success: true
}
})
})
// 如果要测试抛出异常的Promise的话,我们觉得应该这么写
test('返回结果为 404', async () => {
// toThrow() 抛出异常
await expect(fetchDataPromise()).rejects.toThrow()
})
八 Jest 的钩子函数
在jest中,如果测试用例中需要使用到某个对象 或 在执行测试代码的某个时刻需要做一些必要的处理,直接在测试文件中写基础代码是不推荐的,可以使用jest的钩子函数。
钩子函数的作用:在代码执行的某个时刻,会自动运行的一个函数。
简单例子
新建counter.js文件,代码如下:
export default class Counter {
constructor() {
this.number = 0;
}
addOne() {
this.number += 1;
}
minusOne() {
this.number -= 1;
}
}
编写对应的测试用例:counter.test.js文件,代码如下:
import Counter from './counter.js'
const counter = new Counter();
test('测试counter中addOne方法', () => {
counter.addOne();
expect(counter.number).toBe(1)
})
test('测试counter中minusOne方法', () => {
console.log('counter.number:', counter.number)
counter.minusOne();
expect(counter.number).toBe(0)
})
上边的测试用例可以正常执行,但是addOne()函数调用的次数会影响counter.number的值,就会影响到minusOne方法执行结果。如果你想addOne与minusOne方法调用互不影响时,此时就不得不引入jest的钩子函数。
钩子函数:
beforeAll:在所有测试用例执行之前执行
beforeEach:每个测试用例执行前执行,可让每个测试用例中使用的变量互不影响,因为分别为每个测试用例实例化了一个对象
afterAll:等所有测试用例都执行之后执行 ,可以在测试用例执行结束时,做一些处理
afterEach:每个测试用例执行结束时,做一些处理
import Counter from './counter.js'
// 使用类中的方法,首先要实例化
let counter = null
beforeAll(() => {
console.log('所有测试实例运行之前执行')
})
afterAll(() => {
console.log('所有实例运行之后执行')
})
beforeEach(() => {
// 每个测试用例执行前执行,可让每个测试用例中使用的变量互不影响
//因为分别为每个测试用例实例化了一个对象
counter = new Counter();
console.log('每个测试实例运行之前执行')
})
afterEach(() => {
console.log('每个测试实例运行之后执行')
})
test('测试counter中addOne方法', () => {
counter.addOne();
expect(counter.number).toBe(1)
})
test('测试counter中minusOne方法', () => {
console.log('counter.number:', counter.number)
counter.minusOne();
expect(counter.number).toBe(0)
})
所以在beforeEach加上 counter = new Counter(),测试用例使用的变量就互不影响。
根据上边案例实际打印结果可以看出这四个钩子函数的执行顺序,如下:
(1)beforeAll > (2)beforeEach > (3)afterEach > (4)afterAll
钩子函数是在测试用例执行前或执行后执行的
对测试进行分组
如果测试用例比较多,比如一堆测试加法的,一堆测试减法的,那么我们就可以分组
,通过describe()来实现。其实也可以理解所有测试最外层默认包裹了一层describe。
describe('测试加法', () => {
test('测试counter中addOne方法', () => {
counter.addOne();
expect(counter.number).toBe(1)
})
})
describe('测试减法', () => {
test('测试counter中minusOne方法', () => {
console.log('counter.number:', counter.number)
counter.minusOne();
expect(counter.number).toBe(-1)
})
})
钩子函数的作用域
在describe内部和外部都可以写钩子函数,外部的先执行然后执行内部的
beforeAll(() => {
console.log('我是外部的钩子函数');
})
beforeEach(() => {
console.log('我是外部的钩子函数');
})
describe('测试加法', () => {
beforeAll(() => {
console.log('我是内部的钩子函数')
})
test('测试counter中addOne方法', () => {
counter.addOne();
expect(counter.number).toBe(1)
})
})
describe('测试减法', () => {
test('测试counter中minusOne方法', () => {
counter.minusOne();
expect(counter.number).toBe(-1)
})
})
由打印结果可知,写在内层describe中的beforeAll钩子函数只作用了当前的分组,钩子函数是有作用域的,取决于钩子函数位于 describe
中的位置。
但是注意 如果describe 里是beforeAll,外面则是beforeEach,那则会,
所以,如果是同类型函数,则外部执行,那如果是钩子函数执行顺序,那还是按之前的:(1)beforeAll > (2)beforeEach > (3)afterEach > (4)afterAll
test.only()
如果测试用例比较多,而我们已经定位到其中一个测试用例有问题,那么我们可以单独的输出这个测试用例的结果,即test.only()
beforeAll(() => {
console.log('我是外部的钩子函数: beforeAll')
})
beforeEach(() => {
console.log('我是外部的钩子函数:beforeEach');
counter = new Counter()
})
describe('测试加法', () => {
beforeAll(() => {
console.log('我是内部的钩子函数: beforeAll')
})
test.only('测试addOne方法', () => {
counter.addOne();
expect(counter.number).toBe(1);
})
})
describe('测试减法', () => {
test('测试counter中minusOne方法', () => {
counter.minusOne();
expect(counter.number).toBe(-1)
})
})
看结果 测试减法 被忽略
九 Jest中的Snapshot快照测试
当我们在写业务代码或者配置文件的测试用例时,可能会牵扯到不断修改的过程。那么就会面临着一个比较麻烦的问题:就是每次修改业务代码时,都要相对应的修改测试用例中的断言代码。那么如何避免这个问题呢?使用Snapshot快照即可解决这个大麻烦!
Snapshot快照的作用就是:把每次修改后的代码形成快照,与上次代码生成的快照做对比,若有修改部分,则会出现异常快照提示。
1、快照测试
demo.js
export const generateConfig = () => {
return {
host: "http://www.localhost",
port: "8080"
}
}
demo.test.js
import { generateConfig } from './demo'
test('测试 generatoConfig 函数', () => {
expect(generateConfig()).toEqual({
host: "http://www.localhost",
port: '8080'
})
})
当配置项不断增加的时候,就需要不断去更改测试用例。麻烦
所以使用快照测试,简单容易。
test('测试 generatoConfig 函数', () => {
expect(generateConfig()).toMatchSnapshot();
})
第一次执行 npm run test,就会生成__snapshots__ 文件包
toMatchSnapshot() 会为expect 的结果做一个快照并与前面的快照做匹配。(如果前面没有快照那就保存当前生成的快照即可)
这在配置文件的测试的时候是很有用的,因为配置文件,一般不需要变化。
当然,确实要改配置文件,然后要更新快照,也可。
更新配置文件,这时使用u模式可以同时更新快照,
假设配置项中有随机数或者当前时间,如下
export const generateConfig = () => {
return {
host: "http://www.localhost",
port: "80801",
time: new Date()
}
}
那case 中snapshot 就需要使用expect.any() 了,如下
test('测试 generatoConfig 函数', () => {
expect(generateConfig()).toMatchSnapshot({
time: expect.any(Date)
});
})
运行,执行 u,之前使用snapshot 的时候,都会生成一个snapshot 的文件。
2. 行内的Snapshot (toMatchInlineSnapshot)
行内toMatchInlineSnapshot主要是:直接在测试代码里面插入返回值 。使用行内Snapshot的前提是:安装 prettier
npm i prettier -D
安装完成之后,修改测试用例:
test('测试 generatoConfig 函数', () => {
expect(generateConfig()).toMatchInlineSnapshot({
time: expect.any(Date)
});
})
运行测试用例之后,会多出一个参数来,结果如下:
十 Jest中的Mock
1. 为什么要使用Mock函数?
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
2. Mock函数
提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况(mock函数,捕获函数的调用 和 返回结果 以及 this指向 和 调用顺序.)
- 设置函数返回值
- 改变函数的内部实现
在测试中没有提供测试用的数据,那么可以使用jest提供的mock函数,来捕获函数的调用。
const func = jest.fn(); //mock函数,捕获函数的调用
console.log(func.mock)
在mock函数里面包含了什么内容?我们可以通过console.log来查看
calls------ 函数被调用返回的结果
instances----- 函数指向的this原型
invocationCallOrder ------ 函数执行的顺序
results----- 函数执行的结果,为数组类型,有返回的类型和返回的值
{
calls: [ [] ], // 被调用了多少次 以及 传入参数
instances: [ undefined ], //每次func运行时,this的指向
invocationCallOrder: [ ], //函数执行的顺序
results: [ //函数执行的结果,为数组类型,有返回的类型和返回的值
{ type: 'return', value: undefined }
]
}
示例说明:1,2点
补充:
test('测试jest.fn()内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
})
// 断言mockFn执行后返回100
expect(mockFn(10, 10)).toBe(100);
})
test('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为default
expect(result).toBe('default');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
改变内部函数的实现
jest.mock(‘axios’) 让jest对axios做一个模拟. 在测试用例中即可用同步代码模拟数据,不需要发送真实的请求。
mock测试异步axios请求:
3. 异步函数的测试
(1).模拟请求
对于异步函数的测试,Jest中封装了独立的API,通过例子整理分类如下。
目录结构:
|--demo.js
|--demo.test.js
|--package-lock.json
|--package.json
|--node_modules
|--.babelrc
业务代码:demo.js
import axios from 'axios'
export const fetchData = () => {
return axios.get('/').then(res => res.data)
}
测试代码:demo.test.js
import { fetchData } from './demo'
import Axios from 'axios'
jest.mock('axios') // 模拟axios
test('fetchData 测试', () => {
// 模拟请求
Axios.get.mockResolvedValue({
data: "(function(){return '123'})()"
})
return fetchData().then(data => {
expect(eval(data)).toEqual('123');
})
})
在根目录下新建一个模拟文件夹,名字为__mocks__,在__mocks__文件夹下新建demo.js文件。
情景:当我们测试发送异步请求的代码时,我们可以直接通过模拟demo的方式,测试请求方法是否正常执行.
demo.js
|--__mocks__
|--demo.js
|--demo.js
|--demo.test.js
|--package-lock.json
|--package.json
|--node_modules
|--.babelrc
_ mocks _/demo.js
import axios from 'axios'
export const fetchData = () => {
// 发异步请求
return axios.get('/').then(res => res.data)
}
demo.test.js
jest.mock('./demo'); // 使用jest模拟demo,当执行测试用例时,就会去__mocks__文件下去找demo.js
import { fetchData } from './demo' // 通过jest模拟demo之后,再通过import引入fetchData
// 这里引的fetchData是我们模拟的fetchData
test('fetchData 测试', () => {
return fetchData().then(data => {
expect(eval(data)).toEqual('123')
})
})
目录结构和代码同模拟demo一致。
通过修改jest.config.js配置,让其自动模拟进行测试.
首先, 在命令行执行 npx jest --init 让jest的配置文件暴露出来. 把配置文件中的自动模拟项(automock)设置为true.
然后: 把demo.test.js文件中的 jest.mock(’./demo’) 删除掉.
import { fetchData } from './demo'
test('fetchData 测试', () => {
return fetchData().then(data => {
expect(eval(data)).toEqual('123')
})
})
此时,再执行 npm test ,测试用例依然会通过。
注: 当我们把配置项 automock 修改为 true 后,jest就会开启自动模拟功能,就算测试文件中没有声明模拟代码,jest依然会去自动查找根目录中是否有mocks文件的存在,mocks文件夹下是否有相对应的demo.js文件。如果有,那么在使用 import { fetchData } from ‘./demo’ 引入demo时,会拿mocks下的demo代替我们写的业务代码demo被引入。如果没有,则会引入根目录下得我们写的业务文件demo。这就是自动模拟的运行机制。
目录结构:
|--__mocks__
|--demo.js
|--demo.js
|--demo.test.js
|--package-lock.json
|--package.json
|--node_modules
|--.babelrc
demo.js
import axios from 'axios'
// 异步函数
export const fetchData = () => {
// 发异步请求
return axios.get('/').then(res => res.data)
}
// 同步函数
export const getNumber = () => {
return 123;
}
demo.test.js
jest.mock('./demo'); // 使用jest模拟demo,当执行测试用例时,就会去__mocks__文件下去找demo.js
import { fetchData } from './demo'; // 通过jest模拟demo,这里的fetchData是来自于模拟的demo里的
const { getNumber } = jest.requireActual('./demo'); // 这里的getNumber是来自于真正的demo里的
test('fetchData 测试', () => {
return fetchData().then(data => {
expect(eval(data)).toEqual('123');
})
})
test('getNumber 测试', () => {
expect(getNumber()).toBe(123);
})
- jest.requireActual(’./demo’) 引入真正的业务代码文件。(不是通过jest模拟的)
- 对异步函数模拟、对同步函数不模拟,这样既可实现同步函数 和 异步函数的完美测试。
4.取消模拟
jest.unmock('./mock'); // 取消模拟mock
jest.unmock('axios'); // 取消模拟axio
5. mock timers的使用
平时开发中我们经常用到定时器setInterval 或者setTimeout ,现在我们就写一个定时器的测试用例代码如下:
从运行来看,setTimeout 是没有执行到的。
如何解决这个问题呢?
方法一:根据执行的done()来操作
import { lazy } from './mocktimer'
// done(); 操作
test('should call fn after 3s', (done) => {
const callback = jest.fn();
lazy(callback);
setTimeout(() => {
expect(callback).toBeCalled();
done();
}, 3001);
})
方法二:
今天我们学习一种新的解决办法,使用mock timer解决这个问题。jest 提供了mock timer 的功能,不要再使用真实的时间在这里等了,一个假的时间模拟一下就可以了。
首先是jest.useFakeTimers() 的调用,它就告诉jest 在以后的测试中,可以使用假时间。当然只用它还不行,因为它只是表示可以使用,我们还要告诉jest在哪个地方使用,当jest 在测试的时候,到这个地方,它就自动使用假时间。
两个函数,jest.runAllTimers(), 它表示把所有时间都跑完。具体到我们这个测试,我们希望执完lazy(callback) 就调用, 把lazy函数中的3s时间立刻跑完。可以使用jest.runAllTimers();
import { lazy } from './mocktimer'
jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
jest.runAllTimers();//让所有定时器立即执行
expect(callback).toBeCalled();
})
多个定时器的情况
export const lazy = (fn) => {
setTimeout(() => {
fn();
console.log('第一个定时器执行')
setTimeout(() => {
console.log('第二个定时器执行')
}, 3000)
}, 3000);
}
测试用例:
只执行第一个,
jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
//jest.runOnlyPendingTimers() 只执行一个定时操作
jest.runOnlyPendingTimers(); //只执行一个定时操作
expect(callback).toHaveBeenCalledTimes(1)
})
jest.advanceTimer() 快进几秒
jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
// jest.advanceTimer()// 快进几秒
jest.advanceTimersByTime(3000)
expect(callback).toHaveBeenCalledTimes(1)
})
2个定时器都执行,使用jest.advanceTimersByTime(n)快进n时间执行定时,多个advanceTimerByTime连用时,后一个会以前一个的时间为基点,如果不想互相影响,我们可以使用钩子函数beforeEach解决这个问
import { lazy } from './mocktimer'
jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
jest.advanceTimersByTime(3000)
jest.advanceTimersByTime(6000)//第二个的时间以第一个的时间为基数
//expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1)
})
参考:
十一.TDD介绍
测试驱动开发(Test Driven Development,简称TDD)。TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。
测试驱动开发的思想就是“测试的目的是让你知道,什么时候算是完成了。如果你聪明,你就应该先写测试,这样可以及时知道你是否真地完成了。否则,你经常会不知道,到底有哪些功能是真正完成了,离预期目标还差多远。”
TDD 有三层含义:
- Test-Driven Development,测试驱动开发。 Task-Driven
- Development,任务驱动开发,要对问题进行分析并进行任务分解。 Test-Driven
- Design,测试保护下的设计改善。TDD 并不能直接提高设计能力,它只是给你更多机会和保障去改善设计。
TDD 的开发流程
- 编写测试用例
- 运行测试,测试用例无法通过测试
- 编写代码,使测试用例通过测试
- 优化代码,完成开发
- 重复上述步骤
你可能会问,我写一个测试用例,它明显会失败,还要运行一下吗?
是的。你可能以为测试只有成功和失败两种情况,然而,失败有无数多种,运行测试才能保证当前的失败是你期望的失败。
一切都是为了让程序符合预期,这样当出现错误的时候,就能很快定位到错误(它一定是刚刚修改的代码引起的,因为一分钟前代码还是符合我的预期的)。
通过这种方式,节省了大量的调试代码的时间。
TDD 的优势
- 长期减少回归 bug
- 代码质量更好(组织,可维护性)
- 测试覆盖率高
- 错误测试代码不容易出现
TDD + 单元测试适用范围
- 如果要写一个纯库类,跟业务没有关系,非常适合用 TDD。
- 如果是写业务代码,常常由于测试代码中要用功能代码的数据结构,造成耦合性高
十二.案例
1. 简单使用
math.js
function add(a, b) {
return a + b;
}
function del(a, b) {
return a + b;
}
module.exports = {
add,
del
}
main.test.js
const { add, del } = require('./math.js')
test('测试加法3+7:', () => {
expect(add(3, 7)).toBe(10);
})
test('测试减法7-7:', () => {
expect(del(7, 7)).toBe(0);
})
测试结果:可以看到“减法方法报错”
但是呢,如果我们的html
文件引入该js
则会报错说module
是个什么玩意,这么我们的暂时解决方案是try
一下(实际上现在的项目很少直接自己在html
里引用js
,都前端工程化了)
// math.js
try {
module.exports = {
add
}
} catch (e) {}