jest 覆盖率
本文不适合纯白用户,建议先阅读 jest 的Getting Started:jestjs.io/docs/gettin… 再来阅读本文,体感更好。
通过 jest --coverage 也叫 collectCoverage,该指令可以收集测试覆盖率信息并在输出中报告。
截图如下:
-
%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了? -
%Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了? -
%Funcs函数覆盖率(function coverage):是不是每个函数都调用了? -
%Lines行覆盖率(line coverage):是不是每一行都执行了? -
Uncovered Line: 未覆盖到的行数
回调函数
异步场景
例如:
一个的业务场景:请求函数,请求成功后将数据放到callback中,进行数据处理。
demo 如下:
function fetchData(cb) {
setTimeout(() => {
cb('hello world')
}, 1000)
};
测试代码
test('异步常景', (done) => {
fetchData((params) => {
try {
expect(params).toBe('hello world') // ok
done()
} catch (error) {
done(error)
}
})
})
- 此时必须添加
done作为回调的结束,如果不添加done函数,则会提示超时。
因为fetchData调用结束后,此测试就提前结束了。 并未等callback的调用。 - 如果在测试过程中遇到不符合预期的情况,可以使用
try catch将错误的信息传递给 done 的第一个参数。 - 如果不传递错误信息。遇到错误也可以通过测试,此时的测试结果是不准确的。
// 此测试用例也可以通过
test('callback', (done) => {
fetchData((params) => {
try {
throw Error('error');
done()
} catch (error) {
done()
}
})
})
同步回调
test('同步场景',() => {
const fun = (cb) => {
cb && cb()
};
const cbFun = jest.fn();
fun(cbFun);
expect(cbFun).toBeCalled(); // ok
})
异步函数
异步代码如下
function fetchData(type){
return type ? Promise.resolve('success') : Promise.reject('error')
};
测试逻辑
- 通过
promise的方式
此处必须
return,把整个断言作为返回值返回。如果你忘了return语句的话,在fetchData返回的这个promise变更为resolved状态、then()有机会执行之前,测试就已经被视为已经完成了
test('promise rejected', () => {
return expect(fetchData(false)).reject.toBe('error')
})
test('promise resolve', () => {
return expect(fetchData(true)).resolve.toBe('success')
})
- 通过
async / await的方式
test('async resolve', async () => {
let res = await fetchData(true);
expect(res).toBe('success');
})
test('async reject', async () => {
try {
await fetchData(false);
} catch (error) {
expect(error).toMatch('error');
}
})
注意:
expect.assertions(1)推荐添加。如果 fetchData 不执行 reject,测试用例依旧可以通过,但是如果添加expect.assertions(1),则要求此测试用例至少要被运行一次。即 必须mock一次promise.reject的情况
try catch
demo 代码
const tryCatch = (data) => {
try {
data.push(77);
} catch (error) {
console.error('tryCatch', error.message);
return null
}
}
测试逻辑
it('should throw an error if an exception is thrown', () => {
// 使用 spyOn 进行模拟 console 方法,监听 console.error 方法
const spy = jest.spyOn(console, 'error');
const result = tryCatch(123);
// 测试返回值
expect(result).toBeNull();
// 传递的参数第一个是 tryCatch
expect(spy).toHaveBeenCalledWith('tryCatch', expect.stringContaining(''))
// 恢复原来 console.error 方法。(防止影响后续流程)
spy.mockRestore();
});
jest.spyOn 方法可以替换模块中原有的方法。例
jest.spyOn(console,'error').mockImplementation(() => 'this is error')
console.error() // this is error
模拟-模块
模拟-本地模块
1. 方式一
假如我们要模拟一个 index.js 的模块。
首先在 index.js 文件同级目录新建一个名字为 __mocks__ 的文件夹,再创建一个名字与要模拟模块相同的文件。
例如:
├── __mocks__
│ └── index.js
└── index.js
Demo 示例
├── __test__
│ └── index.test.jsx
├── index.jsx
└── service
├── __mocks__
│ └── index.js
└── index.js
// index.jsx
import React from "react";
import localModule from "./service/index.js";
export const InputCom = () => {
return <div>
{localModule.name}
</div>;
};
// service/index.js
module.exports = {
name: 'localModule'
}
// service/__mocks__/index.js
module.exports = {
name: 'mock localModule'
}
// index.test.js
import { render, screen } from "@testing-library/react";
import { LocalTest } from "..";
import React from "react";
import '../service'
jest.mock('../service');
test("测试本地模块", () => {
render(<LocalTest></LocalTest>);
screen.debug()
});
测试结果:
注意:有两个注意事项
1、该__mocks__文件夹区分大小写,因此__MOCKS__在某些系统上命名该目录会中断。 2、我们必须显式调用jest.mock('./moduleName')。
2. 方式二
通过 jest.mock 方法,在文件内进行数据的模拟。不创建 __mocks__ 文件。
我们改写测试文件。
// index.test.js
import { render, screen } from "@testing-library/react";
import { LocalTest } from "..";
import React from "react";
import '../service'
jest.mock('../service',() => ({
name: 'mock localModule'
}));
test("测试本地模块", () => {
render(<LocalTest></LocalTest>);
screen.debug()
});
测试的结果与方式一一样。
模拟-第三方模块
1. 方式一
- 我们在与node_modules 同级目录下创建
__mocks__ - 在
__mocks__下面创建我们需要模拟的第三方模块,格式:@scoped/moduled-name.js,例如: @testing/fun。 我们创建__mocks__/@testing/fun.js文件。
├── package.json
├── node_modules
├── index.js
├── __mocks__
│ └── @testing
│ └── fun.js
├── __test__
│ └── index.test.jsx
├── index.jsx
// index.jsx
import React from "react";
import nodeModule from '@testing/fun';
export const NodeModuleTest = () => {
return <div>
{nodeModule?.name}
</div>;
};
// @testing/fun
module.exports = {
name: 'module-name',
age: 'module-age',
fun: () => 'module-fun'
}
// __mocks__/@testing/fun.js
module.exports = {
name: 'mock-nodeModule',
fun: () => 'mock-fun'
}
//index.test.js
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import NodeModuleTest from '..'
it("测试第三方模块", () => {
render(<NodeModuleTest />)
screen.debug();
});
运行测试用例以后,结果:
⚠️ 注意:
1、模拟第三方模块无须显示调用jest.mock。
2、Node内置的模块,我们需要显示的调用jest.mock例如fs,我们需要手动的调用jest.mock(fs)
2. 方式二
//index.test.js
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import NodeModuleTest from '..'
jest.mock("@testing/fun", () => ({
name: "mock-module-jest.mock",
}));
it("测试第三方模块", () => {
render(<NodeModuleTest />)
screen.debug();
});
运行测试用例以后的结果:
即使有__mocks__/@scoped/project-name.js 文件,在项目中的单独写的 jest.mock 方法权重最高。
我们模拟的时候后,可能有以下的诉求:
假设第三方包导出了 a,b,c 三个方法。
b 方法使用真实方法,其他导出使用模拟的方法。- 仅模拟
a 方法,其他导出使用真是方法。
以上两种情况我们都可以使用 jest.requireActual() 来解决。
// index.jsx
import React from "react";
import nodeModule from '@testing/fun';
export const NodeModuleTest = () => {
return <div>
{nodeModule?.name}
{nodeModule?.age}
{nodeModule?.sex}
</div>;
};
// @testing/fun
module.exports = {
name: 'module-name',
age: 'module-age',
sex: 'module-sex'
}
//index.test.js
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import NodeModuleTest from '..'
jest.mock("@testing/fun", () => {
const originalModule = jest.requireActual("@testing/fun");
return {
// __esModule: true, // 处理 esModules 模块
...originalModule,
name: "mock-module-jest.mock",
}
})
it("测试第三方模块", () => { render(<NodeModuleTest />) screen.debug(); });
函数测试
.mock 属性
在学习函数测试之前,我们需要先了解.mock属性都有哪些?
测试 demo 如下:
test('.mock', () => {
const mockFun = jest.fn(() => {});
console.log(mockFun.mock);
});
结果如下:
实际上是 6 个属性,还有一个
lastCall 方法,这个方法只有在模拟的函数被调用的时候才会存在。
| 属性名称 | 属性含义 |
|---|---|
| calls | 包含对此模拟函数进行的所有调用的调用参数的数组。数组中的每一项都是调用期间传递的参数数组 |
| contexts | 包含模拟函数的所有调用的上下文的数组 |
| instances | 一个数组,其中包含已使用通过 new 模拟函数实例化的所有对象实例 |
| invocationCallOrder | 包含被调用的顺序 |
| results | 包含模拟函数的所有调用的上下文的数组 |
| lastCall | 包含对此模拟函数进行的最后一次调用的调用参数的数组。如果函数没有被调用,它将返回undefined |
光看介绍可能不足以让我们了解到底是如何玩的,继续向下看:
- calls
包含对此模拟函数进行的所有调用的调用参数的数组。数组中的每一项都是调用期间传递的参数数组
//mockProperty.test.js
test('.mock.calls', () => {
const mockFun = jest.fn(() => {});
mockFun(1)
mockFun(2)
console.log("%c Line:27 🍯 mockFun", "color:#b03734", mockFun.mock);
});
如上图所示,
mockFun 方法调用了两次,分别传了1,2两个参数,打印结果中 calls 则是记录每次调用的入参。
即使调用的时候没有传参数, calls 依旧会记录调用的次数。
test('.mock.calls', () => {
const mockFun = jest.fn(() => {});
mockFun()
mockFun()
console.log("%c Line:27 🍯 mockFun", "color:#b03734", mockFun.mock.calls);
});
- contexts
包含模拟函数的所有调用的上下文的数组
test('.mock.contexts', () => {
const mockFn = jest.fn();
const thisContext0 = () => {}
const thisContext1 = () => {}
const thisContext2 = () => {}
const boundMockFn = mockFn.bind(thisContext0);
boundMockFn();
mockFn.call(thisContext1);
mockFn.apply(thisContext2);
console.log("%c Line:55 🍞 mockFn.mock", "color:#ea7e5c", mockFn.mock);
})
打印结果如下
contexts 中记录了本次调用 this 所属上下文
- instances
一个数组,其中包含已使用通过 new 模拟函数实例化的所有对象实例
test('.mock.instances', () => {
const mockFn = jest.fn();
const boundMockFn1 = new mockFn();
const boundMockFn2 = new mockFn();
const boundMockFn3 = new mockFn();
console.log("%c Line:55 🍞 mockFn.mock", "color:#ea7e5c", mockFn.mock.instances[0] == boundMockFn1); // true
console.log("%c Line:55 🍞 mockFn.mock", "color:#ea7e5c", mockFn.mock.instances[1] == boundMockFn2); // true
console.log("%c Line:55 🍞 mockFn.mock", "color:#ea7e5c", mockFn.mock.instances[2] == boundMockFn3); // true
})
- invocationCallOrder
包含被调用的顺序
使用上面的
instance测试方法,打印结果.mock属性,可以在invocationCallOrder方法中看到每个方法被调用的顺序。
- results
包含模拟函数的所有调用的上下文的数组
用的比较多的属性。
'return'- 表示调用正常返回完成。'throw'- 表示调用通过抛出值完成。'incomplete'- 表示呼叫尚未完成。如果您从模拟函数本身或从模拟调用的函数中测试结果,就会发生这种情况。
//type 为 return 的情况
test('.mock.result', () => {
const mockFn = jest.fn(() => {
return ''
});
mockFn(1)
console.log("%c Line:55 🍞 mockFn.mock.result", "color:#ea7e5c", mockFn.mock.results);
})
// type 为 throw
test('.mock.result', () => {
const mockFn = jest.fn(() => {
throw Error('error')
});
try {
mockFn()
} catch (error) {
}
console.log("%c Line:55 🍞 mockFn.mock.result", "color:#ea7e5c", mockFn.mock.results);
})
// type 为incomplete
test('.mock.result', () => {
const mockFn = jest.fn(() => {
console.log("%c Line:55 🍞 mockFn.mock.results", "color:#ea7e5c", mockFn.mock.results);
});
mockFn()
})
- lastCall
包含对此模拟函数进行的最后一次调用的调用参数的数组。如果函数没有被调用,它将返回
undefined
test('.mock.lastCall', () => {
const mockFn = jest.fn(() => {
});
mockFn()
mockFn(2)
console.log("%c Line:55 🍞 mockFn.mock.lastCall", "color:#ea7e5c", mockFn.mock.lastCall);
})
test('.mock.lastCall', () => {
const mockFn = jest.fn(() => {
});
console.log("%c Line:55 🍞 mockFn.mock.lastCall", "color:#ea7e5c", mockFn.mock.lastCall);
})
一个模拟函数f被调用了 3 次,返回'result1',抛出错误,然后返回'result2',将有一个mock.results如下所示的数组:
[
{
type: 'return',
value: 'result1',
},
{
type: 'throw',
value: {
/* Error instance */
},
},
{
type: 'return',
value: 'result2',
},
];
模拟函数
- 创建模拟函数
模拟函数有三种方式
| 方法 | 介绍 |
|---|---|
| jest.mock | 模块的模拟 |
| jest.fn | jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值 |
| jest.spyOn | jest.fn 的语法糖,可以监控模拟函数的调用情况 |
三种方式都可以用来模拟函数。
jest.mock 不再做介绍.
jest.fn
onchange = jest.fn() // 返回 undefined
// 可以模拟实现
onchange = jest.fn(() => console.log('log'));
jest.spyOn
let car = {
stop: () => "stop",
};
jest.spyOn(car,'stop').mockImplementation(() => 'mock-stop'); // mock-stop
-
模拟函数返回
-
mockReturnValue
函数的内容将不会被执行
let mockFun = jest.fn().mockReturnValue('mockReturnValue') mockFun() // mockReturnValue- mockReturnValueOnce
let mockFun = jest.fn(() => 'returnValue').mockReturnValueOnce('mockReturnValue') mockFun() // mockReturnValue mockFun() // returnValue- mockResolvedValue
let mockFun = jest.fn(() => 'returnValue').mockResolvedValue('mockReturnValue') mockFun() // Promise { 'mockReturnValue' }- mockResolvedValueOnce
let mockFun = jest.fn(() => 'returnValue').mockResolvedValue('mockReturnValue') mockFun() // Promise { 'mockReturnValue' } mockFun() // Promise { 'returnValue' }- mockRejectedValue
let mockFun = jest.fn(() => 'returnValue').mockRejectedValue('mockReturnValue') await mockFun().catch(err => { console.log(err) // mockReturnValue })- mockRejectedValueOnce
同上
除了单独使用还可以连在一起使用,例如:
let mockFun = jest.fn(() => 'returnValue') .mockReturnValue('default') .mockReturnValueOnce('first') .mockReturnValueOnce('two') .mockResolvedValueOnce('resolved') .mockRejectedValueOnce('rejected') mockFun(); // first mockFun(); // two mockFun(); // Promise { 'resolved' } mockFun(); // Promise { <rejected> 'rejected' } mockFun(); // default -
测试
一般函数测试主要分为几种:
- 测试函数的调用情况
- 测试函数的返回值
我们在上面已经了解到了mockFun.mock属性,以及mockFun 的创建方式。下面进入本小节的核心,如何测试函数.
-
函数调用 通过
.mocks.calls属性的长度,判断函数是否被调用、调用的次数、调用的参数。- 函数是否被调用
test('.mock.calls', () => { const mockFun = jest.fn(() => {}); mockFun() expect(mockFun.mock.calls.length).toBe(1); mockFun() expect(mockFun.mock.calls.length).toBe(2); // ok });- 函数接收到的参数
test('.mock.calls', () => { const mockFun = jest.fn(() => {}); mockFun(1) mockFun() expect(mockFun.mock.calls[0][0]).toBe(1); // ok expect(mockFun.mock.calls[0][1]).toBeUndefined() // ok }); -
函数返回 通过
.mock.results属性判断函数的结果
test('.mock.calls', () => {
const mockFun = jest.fn((params) => params);
mockFun(1)
mockFun(2)
expect(mockFun.mock.result[0].value).toBe(1); // ok
expect(mockFun.mock.result[1].value).toBe(2); // ok
});
自定义匹配器
在测试一节,可以看到要判断一个函数的调用、返回相对比较麻烦,是否有简单的方式能够断言函数是否调用与返回呢?答案是有的,我们继续往下看:
- 是否被调用
toBeCalled也叫做toHaveBeenCalled
const fun = jest.fn();
expect(fun).toBeCalled() // error
fun();
expect(fun).toBeCalled() // ok
- 调用次数
toHaveBeenCalledTimes - 调用参数
toHaveBeenCalledWith - 是否有返回值(没有抛出错误)
toHaveReturned - 返回值
toHaveReturnedWith
定时器
| API | 作用 |
|---|---|
| useFakeTimers | 对文件中的所有测试使用假计时器,直到原始计时器恢复为jest.useRealTimers(). |
| useRealTimers | 恢复全局日期、性能、时间和计时器 API 的原始实现. |
| runAllTimers | 执行所有挂起的宏任务和微任务. |
| runOnlyPendingTimers | 仅执行当前挂起的宏任务(即,仅执行到此时或setInterval()之前已排队的任务)。如果任何当前挂起的宏任务安排新的宏任务,则这些新任务将不会被此调用执行。 |
| advanceTimersByTime | advanceTimersByTime(n) 所有计时器都会提前n毫秒 |
advanceTimersByTime
- useFakeTimes
对文件中的所有测试使用假计时器,直到原始计时器恢复为
jest.useRealTimers()
1、这是一个全局性的操作。无论是在测试文件的顶部还是某个测试 case 中。
2、会被替换的方法包括Date,performance.now(),queueMicrotask(),setImmediate(),clearImmediate(),setInterval(),clearInterval(),setTimeout(),clearTimeout()。
3、在 Node 环境中process.hrtime,process.nextTick()
4、在 JSDOM 环境中requestAnimationFrame(),cancelAnimationFrame(),requestIdleCallback(),cancelIdleCallback()也会被替换。
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log('Times up -- stop!');
callback && callback();
}, 1000);
}
const callback = jest.fn(() => 'callback');
jest.spyOn(global, 'setTimeout');
timerGame(callback)
expect(setTimeout).toHaveBeenCalledTimes(1); // ok
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); // ok
expect(callback).not.toBeCalled(); // ok
在此 demo 中,
-
通过
spyOn方法监听setTimeout方法 。 -
通过
toHaveBeenCalledTimes判断setTimeout方法是否被执行.
在这个例子中,我们只能确定setTimeout方法是否有被调用,但callback 方法是否在 1 秒之后调用我们并不知道,那么我们该如何确定我们的方法在 1秒之后被调用了呢?就需要用到runAllTimers 方法,我们往下看。
- runAllTimers
执行所有挂起的宏任务和微任务
补充上面的测试案例
jest.useFakeTimers();
const callback = jest.fn(() => 'callback');
jest.spyOn(global, 'setTimeout');
timerGame(callback);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
expect(callback).not.toBeCalled();
jest.runAllTimers();
expect(callback).toBeCalled(); // ok
- runOnlyPendingTimers
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log('Times up -- stop!');
callback && callback();
timerGame(callback)
}, 1000);
}
如果我们的函数中存在递归调用的情况,在运行测试 case 的时候就会不断的执行。而我们的目标只要确定callback是否有被执行, 只需要测试一次即可。改写我们的测试 case 。
jest.useFakeTimers();
const callback = jest.fn(() => 'callback');
jest.spyOn(global, 'setTimeout');
timerGame(callback);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
expect(callback).not.toBeCalled();
jest.runOnlyPendingTimers();
expect(callback).toBeCalled(); // ok
- advanceTimersByTime
jest.useFakeTimers();
const callback = jest.fn(() => 'callback');
jest.spyOn(global, 'setTimeout');
timerGame(callback);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
expect(callback).not.toBeCalled();
//jest.runAllTimers();
//jest.advanceTimersByTime();
expect(callback).toBeCalled(); // ok
在这里我们不再使用 runAllTimes,而是通过 advanceTimersByTime来提前执行callback
详细请看: jestjs.io/docs/jest-o…
喜欢就留个赞再走吧~