以下是本文章內(nèi)容:
- 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對象
函數(shù)講完了,‘生命周期’也說了,現(xiàn)在在聊聊jest這個對象。官網(wǎng)并沒有單獨對jest對象做講解,都是用到再說,因此,我總結(jié)了一下我日常生活中用到的,大家可以當(dāng)作積累來對待,甚至可以跳過這一節(jié)內(nèi)容。

jest對象自動位于每個測試文件的范圍內(nèi)。對象中的方法jest有助于創(chuàng)建模擬并讓您控制 Jest 的整體行為。它也可以通過 via 顯式導(dǎo)入import {jest} from '@jest/globals'。
jest.requireActual()和jest.mock()
jest.requireActural()返回實際模塊而不是模擬,繞過所有關(guān)于模塊是否應(yīng)該接收模擬實現(xiàn)的檢查。常用于獲取模塊本身,與jest.mock一起使用
而jest.mock()模擬具有自動模擬版本的模塊,對于下面的test文件,如果jest.mock()不傳第二參數(shù)(箭頭函數(shù)),那么對應(yīng)路徑下的所有文件將被處于模擬的mock狀態(tài),即他們函數(shù)的本身行為將被抹除,因此當(dāng)我們這樣進行mock的時候jest.mock('../utils/mock/jestFn'),jestFn里面的所有函數(shù)行為變成一個假的mock,即執(zhí)行函數(shù)返回undefined,但是卻沒有mock的那些值(如calls,result等。
但是,當(dāng)我們將jest.requireActual()所返回的源模塊充當(dāng)jest.mock()的第二個回調(diào)參數(shù)的返回值時,此時jest.mock()所mock的各個函數(shù)又變回了初始磨樣,即執(zhí)行jestFn里面的方法返回值不為undefined
溫馨提示:我們的項目都是基于第一節(jié)搭建的,如果說從第一節(jié)開始,順著來的
我們新建文件夾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);
})
})
再繼續(xù)講一點:
jestFn.ts函數(shù)是直接默認(rèn)導(dǎo)出,因此我們模擬mock重新函數(shù)的時候,函數(shù)應(yīng)該放在default中,如果jestFn我們再新加一個foo導(dǎo)出,那么模擬的時候便可以直接寫
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'
}
});
有人可能會問,我為啥要這么做,直接導(dǎo)入不行么?
兩個原因:其一是為了模擬函數(shù)的返回值,或部分實現(xiàn)。例如我們模擬axios,jest不會真的去發(fā)送網(wǎng)絡(luò)請求,那么我們?nèi)绻玫絘xios返回的數(shù)據(jù)并填充到頁面當(dāng)中呢,此時模擬函數(shù)的返回值就很有必要了,這樣我們在頁面渲染的時候,只需要判斷這個函數(shù)執(zhí)行即可。其二是為了測試覆蓋率,雖然我們直接將函數(shù)導(dǎo)入進來模擬也行,但是這樣就無法滿足第一點,那么,而只是將函數(shù)導(dǎo)入進來,又不使用,那么是不會覆蓋到這個jestFn文件的,因此jest.mock()也能提高覆蓋率。
jest.mocked(source,{shallow:true})
jest.mocked()模擬函數(shù)的類型定義和包裝對象的類型及其深層嵌套成員,如果不需要深層次定義,那么shallow:false。翻譯成人話就是給讓一個函數(shù)處于mock狀態(tài),這樣我們就能去設(shè)置他的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函數(shù)的時候,得先把我們需要把對應(yīng)的函數(shù)路徑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)
當(dāng)你想在同一個文件中以不同的方式模擬一個模塊時,他就很有用,即多次mock,但是多次mock需要注意:我們需要在每次mock測試完之后,重置模塊注冊表 。我這里只介紹es6的import導(dǎo)入,如果是require可以參考官網(wǎng)(前端多用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(() => {
// 必須重置模塊,否則無法再次應(yīng)用 doMock 的內(nèi)容
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
});
});
})
})
直接跑就完事,寫則是參照來寫,當(dāng)然,這基本上也是官網(wǎng)的寫法,我只是加上了理解與示例。
jest.spyOn()
創(chuàng)建一個類似于jest.fn但也跟蹤調(diào)用的模擬函數(shù)object[methodName]。返回 Jest模擬函數(shù),可以模擬一個文件中的某個函數(shù),類似部分模擬,同doMock類似,如果想實現(xiàn)多次模擬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
})
})
})
如果需要模擬函數(shù)重新實現(xiàn),則使用mockImplementation,并且我們也結(jié)合了前面的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)
確定給定函數(shù)是否為模擬函數(shù),感覺沒啥用,根據(jù)我的測試,只有使用的函數(shù)是jest.fn()才返回true。加上它是因為我看到別人用過了。。。
jest.spied
也用處不大,并且官方也建議,如果需要模擬函數(shù)的重新實現(xiàn),可以使用mockImplementation,理由也是同上。
jest對象中還包含了計時器,但是在說計時器之前,我們先聊一聊異步的模擬,畢竟計時器就可以說是異步了。
模擬異步
異步運行代碼在 JavaScript 中很常見。當(dāng)您有異步運行的代碼時,Jest 需要知道它正在測試的代碼何時完成,然后才能繼續(xù)進行另一個測試。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對象相關(guān)的測試放置到一個文件夾了

這樣我們就模擬了一個異步了。
我們也可以使用異步的語法糖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
驗證在測試期間調(diào)用了一定數(shù)量的斷言。這在測試異步代碼時通常很有用,以確?;卣{(diào)中的斷言確實被調(diào)用。說人話就是提前預(yù)定有幾個異步,比如我上面只有一個異步,那么就可以這樣寫;而hasAssertions就是判斷是否有異步代碼,額不就等同于assertions(>1)。我認(rèn)為這個有用,在于必須提前知道異步的數(shù)量,那么如何知道呢?一個是項目是你自己寫的,自然可以數(shù)出來有多少個,另一個是直接寫一個很大的值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');
}
})
定時器
當(dāng)我們了解了異步的這個概念之后,現(xiàn)在我們可以開始說定時器。定時器模擬是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);
// 打?。篟eady....go!
})
})
測試我們只會打?。?* Ready....go!**
然后我們運行計時器:jest.runAllTimes(),將執(zhí)行所有掛起的宏任務(wù)和微任務(wù)
測試代碼調(diào)整:
...
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!
})
})
以上便是定時器的使用,附帶兩個官網(wǎng)地址:
定時器:
https://www.jestjs.cn/docs/timer-mocks
api: https://www.jestjs.cn/docs/jest-object#jestadvancetimerstonexttimersteps