以下是本文章内容:
- jest对象
- jest.requireActual()和jest.mock()
- jest.mocked(source,{shallow:true})
- doMock(moduleName,factiory,options)
- jest.spyOn()
- jest.isMockFunction(fn)
- jest.isMockFunction(fn)
- 模拟异步
- async/await
- assertions/hasAssertions
- 定时器
- 启用假计时
jest对象
函数讲完了,‘生命周期’也说了,现在在聊聊jest这个对象。官网并没有单独对jest对象做讲解,都是用到再说,因此,我总结了一下我日常生活中用到的,大家可以当作积累来对待,甚至可以跳过这一节内容。

jest对象自动位于每个测试文件的范围内。对象中的方法jest有助于创建模拟并让您控制 Jest 的整体行为。它也可以通过 via 显式导入import {jest} from '@jest/globals'。
jest.requireActual()和jest.mock()
jest.requireActural()返回实际模块而不是模拟,绕过所有关于模块是否应该接收模拟实现的检查。常用于获取模块本身,与jest.mock一起使用
而jest.mock()模拟具有自动模拟版本的模块,对于下面的test文件,如果jest.mock()不传第二参数(箭头函数),那么对应路径下的所有文件将被处于模拟的mock状态,即他们函数的本身行为将被抹除,因此当我们这样进行mock的时候jest.mock('../utils/mock/jestFn'),jestFn里面的所有函数行为变成一个假的mock,即执行函数返回undefined,但是却没有mock的那些值(如calls,result等。
但是,当我们将jest.requireActual()所返回的源模块充当jest.mock()的第二个回调参数的返回值时,此时jest.mock()所mock的各个函数又变回了初始磨样,即执行jestFn里面的方法返回值不为undefined
温馨提示:我们的项目都是基于第一节搭建的,如果说从第一节开始,顺着来的
我们新建文件夹utils/mock/jestFn.ts
export default {
authorize: () => {
return 'token';
},
isAuthorized: (secret: string) => secret === 'wizard',
};
新建测试_test/testJest.test.tsx
jest.mock('../utils/mock/jestFn',() => {
const originModule = jest.requireActual<typeof import('../utils/mock/jestFn')>('../utils/mock/jestFn');
return {
__esModule: true,
...originModule
}
});
import utils from '../utils/mock/jestFn';
describe('test jest something',() => {
it('test jest object',() => {
const res = utils.authorize();
console.log('ll',res);
})
})
再继续讲一点:
jestFn.ts函数是直接默认导出,因此我们模拟mock重新函数的时候,函数应该放在default中,如果jestFn我们再新加一个foo导出,那么模拟的时候便可以直接写
jestFn.ts
export const foo = () => 'foo';
testJest.test.tsx
jest.mock('../utils/mock/jestFn',() => {
...
return {
...
default: {
authorize: jest.fn(() => 'jack'),
isAuthorized: jest.fn((secret: string) => secret === 'mike')
},
foo: () => 'mock foo'
}
});
有人可能会问,我为啥要这么做,直接导入不行么?
两个原因:其一是为了模拟函数的返回值,或部分实现。例如我们模拟axios,jest不会真的去发送网络请求,那么我们如果拿到axios返回的数据并填充到页面当中呢,此时模拟函数的返回值就很有必要了,这样我们在页面渲染的时候,只需要判断这个函数执行即可。其二是为了测试覆盖率,虽然我们直接将函数导入进来模拟也行,但是这样就无法满足第一点,那么,而只是将函数导入进来,又不使用,那么是不会覆盖到这个jestFn文件的,因此jest.mock()也能提高覆盖率。
jest.mocked(source,{shallow:true})
jest.mocked()模拟函数的类型定义和包装对象的类型及其深层嵌套成员,如果不需要深层次定义,那么shallow:false。翻译成人话就是给让一个函数处于mock状态,这样我们就能去设置他的mockReturnValue了,并且无视他的嵌套。
新建文件utils/mock/jestFn.ts
export const foo = (val: string) => val + 'foo';
export const song = {
one: {
more: {
time: (t: number) => {
return t;
},
},
},
}
export default {
authorize: () => {
return 'token';
},
isAuthorized: (secret: string) => secret === 'wizard',
};
新建文件_test/testJestMock.test.tsx
import { song } from "../utils/mock/jestFn";
// 注意点一
jest.mock('../utils/mock/jestFn');
// 移除console
jest.spyOn(console, 'log').mockReturnValue();
// 注意点二
const mockedSong = jest.mocked<any>(song);
describe('test jest object', () => {
it('tes jest mocked and Mocked',() => {
console.log('---')
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
})
})
这里有几个注意点:首先我们在mocked函数的时候,得先把我们需要把对应的函数路径mock一次;其次由于是ts文件,mocked的时候必须要带上一个any属性,否则会提示我们找不到mockReturnValue属性。
那既然是ts,有没有ts专门用的呢,也有,那就是jest.MockedClass或jest.MockedFunction。jest.MockedObject,这里我是用的是第二个
新建utils/mock/type.ts
export interface LoadDD {
username: string,
age: number
}
在utils/mock/jestFn.ts新增方法
import type { LoadDD } from "./type";
export const loadData = (): LoadDD => {
return {
username: 'jack',
age: 23
}
}
在testJestMock.test.tsx中新增测试
import { song,loadData } from "../utils/mock/jestFn";
jest.mock('../utils/mock/jestFn');
const mock_loadData = loadData as jest.MockedFunction<typeof loadData>
it('test jest MockFunction',() => {
mock_loadData.mockReturnValue({
username: 'jack',
age: 23
})
const res = mock_loadData();
console.log(res,'load data')
})
使用这个方法,我们似乎可以更好的使用类型定义,并且避免了any大法。也许你们会说,不还有一个Mocked么,确实,但是我暂时没用到,而且由于我需要使用类型定义,那么这个方法反而不容易用到了,如需了解,可以访问官方:
https://www.jestjs.cn/docs/mock-function-api。
doMock(moduleName,factiory,options)
当你想在同一个文件中以不同的方式模拟一个模块时,他就很有用,即多次mock,但是多次mock需要注意:我们需要在每次mock测试完之后,重置模块注册表 。我这里只介绍es6的import导入,如果是require可以参考官网(前端多用import):
https://www.jestjs.cn/docs/jest-object#jestunmockmodulename
我们新建测试_test/testDo.test.tsx
import { loadData } from "../utils/mock/jestFn";
// 移除console
jest.spyOn(console, 'log').mockReturnValue();
describe('test jest object', () => {
beforeEach(() => {
// 必须重置模块,否则无法再次应用 doMock 的内容
jest.resetModules();
});
it('test do mock 1',() => {
jest.doMock('../utils/mock/jestFn', () => {
return {
__esModule: true,
default: 'default1',
loadData: () => {
return {
username: 'jack123',
age: 23
}
},
};
});
return import('../utils/mock/jestFn').then(moduleName => {
expect(moduleName.default).toBe('default1');
expect(moduleName.loadData()).toEqual({
username: 'jack123',
age: 23
});
});
})
it('test do mock 2',() => {
jest.doMock('../utils/mock/jestFn', () => {
return {
__esModule: true,
default: 'default1',
loadData: () => {
return {
username: 'jack234',
age: 23
}
},
};
});
return import('../utils/mock/jestFn').then(moduleName => {
expect(moduleName.default).toBe('default1');
expect(moduleName.loadData()).toEqual({
username: 'jack234',
age: 23
});
});
})
})
直接跑就完事,写则是参照来写,当然,这基本上也是官网的写法,我只是加上了理解与示例。
jest.spyOn()
创建一个类似于jest.fn但也跟踪调用的模拟函数object[methodName]。返回 Jest模拟函数,可以模拟一个文件中的某个函数,类似部分模拟,同doMock类似,如果想实现多次模拟mock,那么我们也可以使用spyOn,并且官方也推荐我们使用,毕竟简洁多了.
我们新建一个测试_test/testSpy.test.tsx
import * as JestFn from "../utils/mock/jestFn"
describe('test jest object2', () => {
it('test jest spyOn',() => {
// loadData为JestFn的一个属性
jest.spyOn(JestFn,'loadData').mockReturnValue({
username: 'jack123',
age: 23
})
expect(JestFn.loadData()).toEqual({
username: 'jack123',
age: 23
})
})
it('test jest spyOn2',() => {
// loadData为JestFn的一个属性
jest.spyOn(JestFn,'loadData').mockReturnValue({
username: 'jack1234',
age: 23
})
expect(JestFn.loadData()).toEqual({
username: 'jack1234',
age: 23
})
})
})
如果需要模拟函数重新实现,则使用mockImplementation,并且我们也结合了前面的jestMockedFunction
我们给testSpy.test.tsx加一个测试
import * as JestFn from "../utils/mock/jestFn";
const mock_foo = jest.spyOn(JestFn,'foo') as jest.MockedFunction<typeof JestFn.foo>
mock_foo.mockImplementation(
(val: string) => val + '_foo'
)
describe('test jest object2', () => {
it('test jest spyOn implementation',() => {
const res = mock_foo('zl');
console.log(res,'mock implementation')
})
...
})
jest.isMockFunction(fn)
确定给定函数是否为模拟函数,感觉没啥用,根据我的测试,只有使用的函数是jest.fn()才返回true。加上它是因为我看到别人用过了。。。
jest.spied
也用处不大,并且官方也建议,如果需要模拟函数的重新实现,可以使用mockImplementation,理由也是同上。
jest对象中还包含了计时器,但是在说计时器之前,我们先聊一聊异步的模拟,毕竟计时器就可以说是异步了。
模拟异步
异步运行代码在 JavaScript 中很常见。当您有异步运行的代码时,Jest 需要知道它正在测试的代码何时完成,然后才能继续进行另一个测试。Jest 有几种方法来处理这个问题,都是一个个真实的实例,需要大家亲自验证!
模拟async/await
我们新建文件utils/async/index.ts
export const getDD = (count: number) => {
return new Promise((resolve,reject) => {
if (count > 20) {
reject('count is too large')
} else {
resolve('count is right')
}
})
}
然后新建测试文件_test/jest/testAsync.test/tsx
import { getDD } from "utils/async";
describe('test async',() => {
it('test await/async', async () => {
const res2 = await getDD(10);
expect(res2).toEqual('count is right');
try {
await getDD(10);
} catch (e) {
expect(e).toBe('count is too large');
}
})
})
注:这里我把jest对象相关的测试放置到一个文件夹了

这样我们就模拟了一个异步了。
我们也可以使用异步的语法糖resolves/rejects去判断,语法糖可以用一个await或者return,如下:
describe('test async',() => {
...
it('test syntastic sugar',async () => {
await expect(getDD(10)).resolves.toBe('count is right');
// return expect(getDD(10)).resolves.toBe('count is right');
await expect(getDD(30)).rejects.toBe('count is too large');
// return expect(getDD(30)).rejects.toBe('count is too large');
})
})
assertions/hasAssertions
验证在测试期间调用了一定数量的断言。这在测试异步代码时通常很有用,以确保回调中的断言确实被调用。说人话就是提前预定有几个异步,比如我上面只有一个异步,那么就可以这样写;而hasAssertions就是判断是否有异步代码,额不就等同于assertions(>1)。我认为这个有用,在于必须提前知道异步的数量,那么如何知道呢?一个是项目是你自己写的,自然可以数出来有多少个,另一个是直接写一个很大的值assertions(1000),这样肯定报错,然后报错就会告诉你有几个异步了。。。
it('test await/async', async () => {
expect.hasAssertions();
expect.assertions(1);
const res2 = await getDD(10);
expect(res2).toEqual('count is right');
try {
await getDD(10);
} catch (e) {
expect(e).toBe('count is too large');
}
})
定时器
当我们了解了异步的这个概念之后,现在我们可以开始说定时器。定时器模拟是jest对象介绍的扩展,因为接口也是jest的,但是我这里单独拿出来做讲解,并且在有些时候,定时器模拟会给你带来覆盖率的提升。
启用假计时
新建文件utils/fake/timeGame.ts
type CallBack = () => void
export const timerGame = (callback: CallBack) => {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
新建测试_test/fake/timeGame.test.tsx
import { timerGame } from "utils/fake/timeGame";
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
describe('test fake time',() => {
it('test start fake',() => {
const fn = jest.fn()
timerGame(fn);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function),1000);
// 打印:Ready....go!
})
})
测试我们只会打印:** Ready....go!**
然后我们运行计时器:jest.runAllTimes(),将执行所有挂起的宏任务和微任务
测试代码调整:
...
describe('test fake time',() => {
it('test start fake',() => {
...
expect(fn).not.toBeCalled();
jest.runAllTimers();
expect(fn).toBeCalled();
expect(fn).toBeCalledTimes(1);
// 此时先后打印:
// Ready....go!
// Time's up -- stop!
})
})
以上便是定时器的使用,附带两个官网地址:
定时器:
https://www.jestjs.cn/docs/timer-mocks
api: https://www.jestjs.cn/docs/jest-object#jestadvancetimerstonexttimersteps