詳解Jest?如何支持異步及時間函數(shù)實現(xiàn)示例
異步支持
在前端開發(fā)中,我們會遇到很多異步代碼,那么就需要測試框架對異步必須支持,那如何支持呢?
Jest 支持異步有兩種方式:回調(diào)函數(shù)及 promise(async/await)
。
回調(diào)函數(shù) callback
const fetchUser = (cb) => { setTimeout(() => { cb('hello') }, 100) } // 必須要使用done,done表示執(zhí)行done函數(shù)后,測試結(jié)束。如果沒有done,同步代碼執(zhí)行完后,測試就執(zhí)行完了,測試不會等待異步代碼。 test('test callback', (done) => { fetchUser((data) => { expect(data).toBe('hello') done() }) })
需要注意的是,必須使用 done 來告訴測試用例什么時候結(jié)束,即執(zhí)行 done() 之后測試用例才結(jié)束。
promise
const userPromise = () => Promise.resolve('hello') test('test promise', () => { // 必須要用return返回出去,否則測試會提早結(jié)束,也不會進(jìn)入到異步代碼里面進(jìn)行測試 return userPromise().then(data => { expect(data).toBe('hello') }) }) // async test('test async', async () => { const data = await userPromise() expect(data).toBe('hello') })
針對 promise,Jest 框架提供了一種簡化的寫法,即 expect 的resolves
和rejects
表示返回的結(jié)果:
const userPromise = () => Promise.resolve('hello') test('test with resolve', () => { return expect(userPromise()).resolves.toBe('hello') }) const rejectPromise = () => Promise.reject('error') test('test with reject', () => { return expect(rejectPromise()).rejects.toBe('error') })
Mock Timer
基本使用
假如現(xiàn)在有一個函數(shù) src/utils/after1000ms.ts
,它的作用是在 1000ms 后執(zhí)行傳入的 callback
:
const after1000ms = (callback) => { console.log("準(zhǔn)備計時"); setTimeout(() => { console.log("午時已到"); callback && callback(); }, 1000); };
如果不 Mock 時間,那么我們就得寫這樣的用例:
describe("after1000ms", () => { it("可以在 1000ms 后自動執(zhí)行函數(shù)", (done) => { after1000ms(() => { expect(...); done(); }); }); });
這樣我們得死等 1000 毫秒才能跑這完這個用例,這非常不合理,現(xiàn)在來看看官方的解決方法:
const fetchUser = (cb) => { setTimeout(() => { cb('hello') }, 1000) } // jest用來接管所有的時間函數(shù) jest.useFakeTimers() jest.spyOn(global, 'setTimeout') test('test callback after one second', () => { const callback = jest.fn() fetchUser(callback) expect(callback).not.toHaveBeenCalled() // setTimeout被調(diào)用了,因為被jest接管了 expect(setTimeout).toHaveBeenCalledTimes(1) expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000) // 跑完所有的時間函數(shù) jest.runAllTimers() expect(callback).toHaveBeenCalled() expect(callback).toHaveBeenCalledWith('hello') })
runAllTimers是對所有的timer的進(jìn)行執(zhí)行,但是我們?nèi)绻枰?xì)粒度的控制,可以使用 runOnlyPendingTimers
:
const loopFetchUser = (cb: any) => { setTimeout(() => { cb('one') setTimeout(() => { cb('two') }, 2000) }, 1000) } jest.useFakeTimers() jest.spyOn(global, 'setTimeout') test('test callback in loop', () => { const callback = jest.fn() loopFetchUser(callback) expect(callback).not.toHaveBeenCalled() // jest.runAllTimers() // expect(callback).toHaveBeenCalledTimes(2) // 第一次時間函數(shù)調(diào)用完的時機(jī) jest.runOnlyPendingTimers() expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('one') // 第二次時間函數(shù)調(diào)用 jest.runOnlyPendingTimers() expect(callback).toHaveBeenCalledTimes(2) expect(callback).toHaveBeenCalledWith('two') })
我們還可以定義時間來控制程序的運行:
// 可以自己定義時間的前進(jìn),比如時間過去500ms后,函數(shù)調(diào)用情況 test('test callback with advance timer', () => { const callback = jest.fn() loopFetchUser(callback) expect(callback).not.toHaveBeenCalled() jest.advanceTimersByTime(500) jest.advanceTimersByTime(500) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('one') jest.advanceTimersByTime(2000) expect(callback).toHaveBeenCalledTimes(2) expect(callback).toHaveBeenCalledWith('two') })
模擬時鐘的機(jī)制
Jest 是如何模擬 setTimeout
等時間函數(shù)的呢?
我們從上面這個用例多少能猜得出:Jest "好像" 用了一個數(shù)組記錄 callback
,然后在 jest.runAllTimers
時把數(shù)組里的 callback
都執(zhí)行, 偽代碼可能是這樣的:
setTimeout(callback) // Mock 的背后 -> callbackList.push(callback) jest.runAllTimers() // 執(zhí)行 -> callbackList.forEach(callback => callback())
可是話說回來,setTimeout
本質(zhì)上不也是用一個 "小本本" 記錄這些 callback
,然后在 1000ms
后執(zhí)行的么?
那么,我們可以提出這樣一個猜想:調(diào)用 jest.useFakeTimers
時,setTimeout
并沒有把 callback
記錄到 setTimeout
的 "小本本" 上,而是記在了 Jest 的 "小本本" 上!
所以,callback
執(zhí)行的時機(jī)也從 "1000ms
后" 變成了 Jest 執(zhí)行 "小本本" 之時 。而 Jest 提供給我們的就是執(zhí)行這個 "小本本" 的時機(jī)就是執(zhí)行runAllTimers
的時機(jī)。
典型案例
學(xué)過 Java 的同學(xué)都知道 Java 有一個 sleep
方法,可以讓程序睡上個幾秒再繼續(xù)做別的。雖然 JavaScript 沒有這個函數(shù), 但我們可以利用 Promise
以及 setTimeout
來實現(xiàn)類似的效果。
const sleep = (ms: number) => { return new Promise(resolve => { setTimeout(resolve, ms); }) }
理論上,我們會這么用:
console.log('開始'); // 準(zhǔn)備 await sleep(1000); // 睡 1 秒 console.log('結(jié)束'); // 睡醒
在寫測試時,我們可以寫一個 act
內(nèi)部函數(shù)來構(gòu)造這樣的使用場景:
import sleep from "utils/sleep"; describe('sleep', () => { beforeAll(() => { jest.useFakeTimers() jest.spyOn(global, 'setTimeout') }) it('可以睡眠 1000ms', async () => { const callback = jest.fn(); const act = async () => { await sleep(1000) callback(); } act() expect(callback).not.toHaveBeenCalled(); jest.runAllTimers(); expect(callback).toHaveBeenCalledTimes(1); }) })
上面的用例很簡單:在 "快進(jìn)時間" 之前檢查 callback
沒有被調(diào)用,調(diào)用 jest.runAllTimers
后,理論上 callback
會被執(zhí)行一次。
然而,當(dāng)我們跑這個用例時會發(fā)現(xiàn)最后一行的 expect(callback).toHaveBeenCalledTimes(1);
會報錯,發(fā)現(xiàn)根本沒有調(diào)用,調(diào)用次數(shù)為0:
問題分析
這就涉及到 javascript 的事件循環(huán)機(jī)制了。
首先來復(fù)習(xí)下 async / await
, 它是 Promise
的語法糖,async
會返回一個 Promise
,而 await
則會把剩下的代碼包裹在 then
的回調(diào)里,比如:
await hello() console.log(1) // 等同于 hello().then(() => { console.log(1) })
重點:await后面的代碼相當(dāng)于放在promise.then的回調(diào)中
這里用了 useFakeTimers
,所以 setTimeout
會替換成了 Jest 的 setTimeout
(被 Jest 接管)。當(dāng)執(zhí)行 jest.runAllTimers()
后,也就是執(zhí)行resolve
:
const sleep = (ms: number) => { return new Promise(resolve => { setTimeout(resolve, ms); }) }
此時會把 await
后面的代碼推入到微任務(wù)隊列中。
然后繼續(xù)執(zhí)行本次宏任務(wù)中的代碼,即expect(callback).toHaveBeenCalledTimes(1)
,這時候callback
肯定沒有執(zhí)行。本次宏任務(wù)執(zhí)行完后,開始執(zhí)行微任務(wù)隊列中的任務(wù),即執(zhí)行callback
。
解決方法
describe('sleep', () => { beforeAll(() => { jest.useFakeTimers() jest.spyOn(global, 'setTimeout') }) it('可以睡眠 1000ms', async () => { const callback = jest.fn() const act = async () => { await sleep(1000) callback() } const promise = act() expect(callback).not.toHaveBeenCalled() jest.runAllTimers() await promise expect(callback).toHaveBeenCalledTimes(1) }) })
async
函數(shù)會返回一個promise
,我們在promise
前面加一個await
,那么后面的代碼就相當(dāng)于:
await promise expect(callback).toHaveBeenCalledTimes(1) 等價于 promise.then(() => { expect(callback).toHaveBeenCalledTimes(1) })
所以,這個時候就能正確的測試。
總結(jié)
Jest 對于異步的支持有兩種方式:回調(diào)函數(shù)和promise
。其中回調(diào)函數(shù)執(zhí)行后,后面必須執(zhí)行done
函數(shù),表示此時測試才結(jié)束。同理,promise
的方式必須要通過return
返回。
Jest 對時間函數(shù)的支持是接管真正的時間函數(shù),把回調(diào)函數(shù)添加到一個數(shù)組中,當(dāng)調(diào)用runAllTimers()
時就執(zhí)行數(shù)組中的回調(diào)函數(shù)。
最后通過一個典型案例,結(jié)合異步和setTimeout
來實踐真實的測試。
以上就是詳解Jest 如何支持異步及時間函數(shù)實現(xiàn)示例的詳細(xì)內(nèi)容,更多關(guān)于Jest 支持異步時間函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript復(fù)原何同學(xué)B站頭圖細(xì)節(jié)示例詳解
這篇文章主要為大家介紹了JavaScript復(fù)原何同學(xué)B站頭圖細(xì)節(jié)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07JavaScript中Map與Object應(yīng)用場景
這篇文章主要為大家介紹了JavaScript中Map與Object應(yīng)用場景的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07JavaScript深拷貝方法structuredClone使用
這篇文章主要為大家介紹了JavaScript深拷貝方法structuredClone使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02微信小程序 實戰(zhàn)實例開發(fā)流程詳細(xì)介紹
這篇文章主要介紹了微信小程序 實戰(zhàn)實例開發(fā)流程詳細(xì)介紹的相關(guān)資料,這里主要介紹微信小程序的開發(fā)流程和簡單實例,需要的朋友可以參考下2017-01-01