jest基础
匹配器
jest默认环境是node
如果想在jest环境使用esmodule,需要借助@babel/core转化工具, @babel/preset-env指明如何转化
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
只需要一个项目中安装jest,然后执行 npx jest --watch,jest就会自动将所有的.test.js结尾的文件中的测试用例执行。
jest提供了很多匹配器,最基本的使用
test('测试加法', ()=>{
// toBe 类似 Object.is(a,b),适用于普通值
expect(10).toBe(10)
})
test('测试内容', ()=>{
// 匹配对象使用toEqual,递归遍历使用toBe, 加上.not就是取反
expect({a: 1}).toEqual({a: 1})
})
如toBe, toEqual,等等还有很多。当执行npx jest --watch的时候,jest就会执行这些测试用例并返回结果。
PASS ./9.test.js
√ 测试加法 (2ms)
√ 测试内容 (1ms)
Snapshot Summary
› 1 snapshot file obsolete from 1 test suite. To remove it, press `u`.
↳ • __snapshots__学习\demo.test.js.snap
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 file obsolete, 0 total
Time: 0.987s
Ran all test suites related to changed files.
测试通过。
还有更多的适配器如:
expect(10).toBeGreaterThan(9) //大于9
expect( 0.1 + 0.2).toBeCloseTo(0.3) //解决精度问题
expect('abcd').toMatch(/ab/) //匹配字符串
expect([1,2]).toContaine(1) //数组或者集合是否包含某个数
expect(()=>{throw new Error('123')}).toThrow() //抛出异常, 没有toThrow不会抛出异常
expect(()=>{throw new Error('123')}).toThrow('123') //是否抛出123异常,是的话测试通过
更多适配器可以查看官网。
命令行工具
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 file obsolete, 0 total
Time: 2.302s, estimated 3s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
执行完后,下面会提示输入W显示更多指令
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press u to update failing snapshots.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
-
a执行所有测试 默认 jest --watchAll 就是打开了a模式
-
f只测试失败的测试
-
p 根据文件名字正则表达式,匹配更改的test
-
t 根据文件名字,过滤某些test
-
u 更改失败的快照(后面会讲)
-
o 配合git,只执行有修改的文件 默认–watch就是打开了。
测试异步
// 默契请求的方法
const fetchData = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ success: true });
}, 2000);
});
export { fetchData };
因为默认情况下,Jest 测试在执行结束后完成。
test('the data is peanut butter', () => {
fetchData().then(res=>{
expect(res).toEqual({success: true})
})
});
那么其实不管输入什么,该test就会执行正确。
正确测试异步方法:
1 done
test("fetchData 返回结果为{success: true}", (done) => {
fetchData()
.then((res) => {
expect(res).toEqual({ success: true });
done();
})
.catch((err) => {
done(err);
});
});
只有调用done,才会正确测试该test。
2 promise
正常使用promise.then,但是一定要return。而且可以使用expect.assertions(1)表示至少执行一次expect。
因为有些场景,我们只俘获了catch,并无俘获then,那么Promsie成功的时候,这个测试将会没有执行。所以需要这个来确定至少执行一次。否则失败。
test("fetchData 返回结果为{success: true}", () => {
expect.assertions(1) //至少执行一次expect,如果没跑也会报错
return fetchData()
.then((res) => {
expect(res).toEqual({ success: true });
})
.catch((err) => {
expect(err).toMatch("error");
});
});
3 resolves/reject
// 可以.resolves在您的期望语句中使用匹配器,Jest 将等待该承诺解决。如果 promise 被拒绝,测试将自动失败。
test("fetchData 返回结果为{success: true}", () => {
return expect(fetchData()).resolves.toMatchObject({
success: true,
});
// 期望一个 Promise 被拒绝,使用.rejects匹配器。如果promise被拒绝,就执行,如果promise成功执行,则该jest默认失败
return expect(fetchData()).rejects.toThrow()
});
4 asycn/await
记得错误处理
test("fetchData 返回结果为{success: true}", async () => {
try {
const data = await fetchData();
expect(data).toEqual({
success: true,
});
} catch (err) {
// 抛出异常
expect(err).toThrow()
}
});
钩子函数
jest在执行test用例的时候,提供了很多钩子,用来初始化一些东西或者做一些销毁工作,如执行前调用,执行后调用等等。如
beforeAll(()=>{
console.log('beforeAll');
})
afterAll(()=>{
console.log('afterAll');
})
他们分别会在所有的钩子执行前执行和执行后执行。
beforeEach(()=>{
console.log('beforeEach');
})
afterEach(()=>{
console.log('afterEach');
})
beforeEach和afterEach分别会在每个test执行之前和之后运行。
分组
相同的意义的测试用例可以写在同一个分组中,相当于ts的namespace,jest提供了describe
describe("测试加法", () => {
beforeAll(() => {
console.log("内部 eforeAll");
});
afterAll(() => {
console.log("内部 afterAll");
});
afterEach(() => {
console.log("内部 afterEach");
});
beforeEach(() => {
console.log("测试加法 beforeEach");
counter = new Counter();
});
test("test Counter addOne", () => {
counter.addOne();
expect(counter.number).toBe(1);
console.log("addOne");
});
});
如,describe内部的钩子,只会在内部执行。而整个test文件,可以看作是外层包装了一个descrbie(‘xx’,()=>{// test文件的内容})
那么desceibe内部的钩子和外部的钩子,执行顺序是怎样呢?
以下面的为例子
beforeAll(() => {
console.log("外部 beforeAll");
});
afterAll(() => {
console.log("afterAll");
});
beforeEach(() => {
console.log("外部 beforeEach");
});
afterEach(() => {
console.log("外部 afterEach");
});
describe("测试加法", () => {
beforeAll(() => {
console.log("内部 eforeAll");
});
afterAll(() => {
console.log("内部 afterAll");
});
afterEach(() => {
console.log("内部 afterEach");
});
beforeEach(() => {
console.log("测试加法 beforeEach");
});
test("test Counter addOne", () => {
console.log("descrbie内部的测试 addOne");
});
});
执行顺序就是
外部 beforeAll
内部 beforeAll
外部 beforeEach
测试加法 beforeEach
descrbie内部的测试 addOne
内部 afterEach
外部 afterEach
内部 afterAll
外部 afterAll
技巧就是:外部beforeAl > 内部beforeAll > 外部beforeEach > 内部beforeEach > test执行 > 内部afterEach > 外部 afterEach > 内部 afterAll > 外部 afterAll
mock
jest提供了mock的功能,可以模拟请求数据的函数,模拟返回的数据。
其中
jest.fn 返回一个mock函数,1 捕获函数的调用和返回结果, 2 可以自由地设置返回结果 3模拟axios请求返回特定数据
test("测试", () => {
const func = jest.fn((a) => {
return a + 4;
});
// func.xxx有很多语法。
// func.mockImplementtation((a)=>{return a+4})跟jest.fn(fn)的fn一样,函数执行的时候的逻辑
// func.mockImplementationOnce(()=>{}) 只执行一次。
// mockReturnValue('a') 每次调用都返回a
func.mockReturnValueOnce("11111").mockReturnValueOnce("2222"); //第1,2次执行func的时候,返回了1111, 2222
func(1);
func(2);
func(3);
func(4);
// func是否被执行过
//expect(func).toBeCalled()
//expect(func.mock.calls[0]).toEqual([1])
expect(func.mock.results);
console.log(func.mock);
/**
console.log(func.mock);
{可以通过func.mock.calls.length判断调用多少次,以及传参, fn.results判断返回值
calls: [ [ 1 ], [ 2 ], [ 3 ], [ 4 ] ],
instances: [ undefined, undefined, undefined, undefined ], func运行的时候,this指向
invocationCallOrder: [ 1, 2, 3, 4 ],
results: [
{ type: 'return', value: '11111' },
{ type: 'return', value: '2222' },
{ type: 'return', value: 7 },
{ type: 'return', value: 8 }
]
}
});
func执行的时候,实际上是jest.fn()在执行,func.mock提供了很多内部,包括调用多少次,每次的入参,每次的返回等等。并且可以通过func.mockReturnValueOnce(‘xx’)默契一次func调用的时候的返回。
jest.mock可以改变函数的实现
// 3 改变函数的内部实现
import axios from 'axios'
jest.mock('axios')
test.only('测试 axios',async()=>{
axios.get.mockResolvedValue({data: '123'}) //模式请求返回数据
console.log('axios', axios);
await axios.get('xxx').then(data=>{
console.log(data);
expect(data).toEqual({data: '123'}) //断言
})
})
jest.mock(‘axios’)可以将axios上的所有属性变成jest.fn。打印出来的axios是
console.log 9.test.js:5
axios <ref *1> [Function: wrap] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
....
}
axios已经被改造了。可以通过axios.get.mockResolvedValue({data: ‘123’})模式get请求返回的数据,测试如下:
PASS ./9.test.js
√ 测试 axios (10ms)
console.log 9.test.js:6
{ data: '123' }
Snapshot Summary
› 1 snapshot file obsolete from 1 test suite. To remove it, press `u`.
↳ • __snapshots__学习\demo.test.js.snap
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 file obsolete, 0 total
Time: 2.568s, estimated 3s
Ran all test suites related to changed files.
测试通过,get请求返回的data就是{data: ‘123’}
自己改造函数
jest执行测试用例的时候,并不希望真正的发送请求。所以可以通过jest.mock模拟。
假设fetchData是代码中真正发送请求的函数,我们想验证这个函数的正确性,又不想发送请求。可以在根目录下创建__mock__
文件夹,命名相同的文件名字,如demo.js然后实现
// __mock__/demo.js
const fetchData = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ success: true });
}, 2000);
});
export {fetchData }
接着引入fetchData的时候
import {fetchData} from './demo'
jest.mock('./demo') //该语句的用途是去__mock__下面找demo文件,然后看有没有导出的方法,这样上一条语句导入的fetchData就变成了从__mock__/demo.js中导入的我们自己模拟的fetchData,然后去执行。
// 有多个引入的时候,可以通过const {aa} = jest.requireActual('./demo'),这样,aa就回去真正的demo文件下引入了。
test('xxx',()=>{
return fetchData().then(res=>.....)
})
其次,如果jest.mock(’./demo’)找不到__mock__
下的文件的时候,jest内部就会自动模拟,如上面的axios。
mock模拟类
// util.js
class Util {
a(){
setTimeout(()=>{
console.log(123);
}, 3000)
}
b(){
setTimeout(()=>{
console.log(123);
}, 5000)
}
}
export default Util
// test.js
import Util from './util'
jest.mock('./util') //因为我们没有在__mock__下定义该文件, jest.mock发现util是一个类,就自动把类的构造函数和方法变成Jest.fn()
// const Util = jest.fn()
// Util.prototype.a = jest.fn()
// Util.prototype.b = jest.fn()
我们也可以自己定义
// __mock__/util.js
const Util = jest.fn()
Util.prototype.a = jest.fn()
Util.prototype.b = jest.fn()
这样jest.mock(’./util’)就会来这里找。
甚至可以
// test.js
jest.mock('./util', ()=>{
const Util = jest.fn()
Util.prototype.a = jest.fn()
Util.prototype.b = jest.fn()
return Util
})
作为第二个参数,在里面定义并且返回。这就是jest.mock的用法。
定时器
除了可以模拟方法,数据,还可以模拟定时器,因为真正的测试不可能等代码中所有的定时器都执行完,那会花费大量的时间。
- jest.useFakeTimers(); //遇到setTimoeut等定时器就是用模拟的timers
- 在调用有timer的函数的时候加上jest.runAllTimers(),它会立即将timer立即完成。
- runAllTimers会将所有的timer都立即完成,而runOnlyPedingTimers(),将当前处于队列中的timer完成。
- jest.advanceTimersByTime(3000) 让时间快进3s
const fetchData = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ success: true });
}, 5000);
});
// 多个test,每个执行前都初始化,每个test都隔离。定时器之间就不会相互影响。
beforeEach(()=>{
jest.useFakeTimers(); //遇到setTimoeut等定时器就是用模拟的timers
})
afterEach(() => {
// 结束后需要手动清理
jest.useRealTimers();
});
如上,在每一个test执行之前,就需要初始化使用假的timer。然后结束的时候需要手动清理。
test("模拟定时器", (done) => {
fetchData()
.then((res) => {
expect(res).toEqual({ success: true });
done();
})
.catch((err) => {
done(err);
});
jest.runAllTimers() //将所有定时器立即执行完,无需等待,但是如果遇到定时器内嵌定时器的,会将内嵌的定时器也完成
jest.runOnlyPedingTimers() // 只执行当前在等待队列的定时器,内嵌的定时器不会立即完成。
jest.advanceTimersByTime(10000); //快进10s,可以定义多个。
});
快照
快照,顾名思义,就是现在拍照,然后过一阵时间再拍照,对比两个的差异。jest的快照也是如此
假设有个方法
const generateConfig = () => {
return {
server: "http://localhost",
port: 8080,
//time: new Date()
};
};
传统的测试
test('测试generateConfig',()=>{
expect(generateConfig()).toEqual({
server: 'http://localhost',
port:8080
})
})
快照测试
test('测试generateConfig',()=>{
// 匹配快照
expect(generateConfig()).toMatchSnapshot({
time: expect.any(Date)
});
})
第一次执行这个test的时候,会在根目录下创建_snapshots_文件,保存着你的快照
如
// __snapshots__/demo.test.js.snap 快照文件
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`测试generateConfig 1`] = `
Object {
"port": 8080,
"server": "http://localhost",
}
`;
第二次执行的时候,就会跟之前保存的快照进行比较。如果不同就报错。
但是如上,time: new Date()
等动态数据怎么执行都不可能跟上次一样,所以jest提供了参数,如
expect(generateConfig()).toMatchSnapshot({
time: expect.any(Date)
});
只要time匹配到一个Date的类型就可以。
此外,如果遇到不一样的快照,可以输入命令行工具
u 可更新快照全部
i 交互式的一个一个快照进行更新
toMatchInlineSnapshot行内快照,即不生成snapshot文件夹,而是将快照内嵌在test.js文件中,需要搭配安装prettier@1.18.2。如
test("测试generateConfig", () => {
// 匹配快照
expect(generateConfig()).toMatchInlineSnapshot(
{
time: expect.any(Date)
},
);
});
第一次执行之后
test("测试generateConfig", () => {
// 匹配快照
expect(generateConfig()).toMatchInlineSnapshot(
{
time: expect.any(Date)
},
// 执行test生成的快照
`
Object {
"port": 8080,
"server": "http://localhost",
"time": Any<Date>,
}
`
);
});
这就是快照的基本用法。
dom
import * as jsdom from 'jsdom'
var $ = require('jquery')(new jsdom.JSDOM().window);
const addDivToBody = () => {
$("body").append("<div>123</div>");
};
// node不具备dom,而jest在node环境下自己模拟了一套dom的api, jsdom
test("测试 addDivToBody", () => {
addDivToBody();
addDivToBody();
expect($("body").find('div').length).toBe(2)
});
jest在node环境下自己模拟了一套dom的api, jsdom,所以直接使用dom的一些操作。