<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在前端開發中,我們會遇到很多非同步程式碼,那麼就需要測試框架對非同步必須支援,那如何支援呢?
Jest 支援非同步有兩種方式:回撥函數及 promise(async/await)
。
const fetchUser = (cb) => { setTimeout(() => { cb('hello') }, 100) } // 必須要使用done,done表示執行done函數後,測試結束。如果沒有done,同步程式碼執行完後,測試就執行完了,測試不會等待非同步程式碼。 test('test callback', (done) => { fetchUser((data) => { expect(data).toBe('hello') done() }) })
需要注意的是,必須使用 done 來告訴測試用例什麼時候結束,即執行 done() 之後測試用例才結束。
const userPromise = () => Promise.resolve('hello') test('test promise', () => { // 必須要用return返回出去,否則測試會提早結束,也不會進入到非同步程式碼裡面進行測試 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
表示返回的結果:
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') })
假如現在有一個函數 src/utils/after1000ms.ts
,它的作用是在 1000ms 後執行傳入的 callback
:
const after1000ms = (callback) => { console.log("準備計時"); setTimeout(() => { console.log("午時已到"); callback && callback(); }, 1000); };
如果不 Mock 時間,那麼我們就得寫這樣的用例:
describe("after1000ms", () => { it("可以在 1000ms 後自動執行函數", (done) => { after1000ms(() => { expect(...); done(); }); }); });
這樣我們得死等 1000 毫秒才能跑這完這個用例,這非常不合理,現在來看看官方的解決方法:
const fetchUser = (cb) => { setTimeout(() => { cb('hello') }, 1000) } // jest用來接管所有的時間函數 jest.useFakeTimers() jest.spyOn(global, 'setTimeout') test('test callback after one second', () => { const callback = jest.fn() fetchUser(callback) expect(callback).not.toHaveBeenCalled() // setTimeout被呼叫了,因為被jest接管了 expect(setTimeout).toHaveBeenCalledTimes(1) expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000) // 跑完所有的時間函數 jest.runAllTimers() expect(callback).toHaveBeenCalled() expect(callback).toHaveBeenCalledWith('hello') })
runAllTimers是對所有的timer的進行執行,但是我們如果需要更細粒度的控制,可以使用 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) // 第一次時間函數呼叫完的時機 jest.runOnlyPendingTimers() expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('one') // 第二次時間函數呼叫 jest.runOnlyPendingTimers() expect(callback).toHaveBeenCalledTimes(2) expect(callback).toHaveBeenCalledWith('two') })
我們還可以定義時間來控制程式的執行:
// 可以自己定義時間的前進,比如時間過去500ms後,函數呼叫情況 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') })
Jest 是如何模擬 setTimeout
等時間函數的呢?
我們從上面這個用例多少能猜得出:Jest "好像" 用了一個陣列記錄 callback
,然後在 jest.runAllTimers
時把陣列裡的 callback
都執行, 虛擬碼可能是這樣的:
setTimeout(callback) // Mock 的背後 -> callbackList.push(callback) jest.runAllTimers() // 執行 -> callbackList.forEach(callback => callback())
可是話說回來,setTimeout
本質上不也是用一個 "小本本" 記錄這些 callback
,然後在 1000ms
後執行的麼?
那麼,我們可以提出這樣一個猜想:呼叫 jest.useFakeTimers
時,setTimeout
並沒有把 callback
記錄到 setTimeout
的 "小本本" 上,而是記在了 Jest 的 "小本本" 上!
所以,callback
執行的時機也從 "1000ms
後" 變成了 Jest 執行 "小本本" 之時 。而 Jest 提供給我們的就是執行這個 "小本本" 的時機就是執行runAllTimers
的時機。
學過 Java 的同學都知道 Java 有一個 sleep
方法,可以讓程式睡上個幾秒再繼續做別的。雖然 JavaScript 沒有這個函數, 但我們可以利用 Promise
以及 setTimeout
來實現類似的效果。
const sleep = (ms: number) => { return new Promise(resolve => { setTimeout(resolve, ms); }) }
理論上,我們會這麼用:
console.log('開始'); // 準備 await sleep(1000); // 睡 1 秒 console.log('結束'); // 睡醒
在寫測試時,我們可以寫一個 act
內部函數來構造這樣的使用場景:
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); }) })
上面的用例很簡單:在 "快進時間" 之前檢查 callback
沒有被呼叫,呼叫 jest.runAllTimers
後,理論上 callback
會被執行一次。
然而,當我們跑這個用例時會發現最後一行的 expect(callback).toHaveBeenCalledTimes(1);
會報錯,發現根本沒有呼叫,呼叫次數為0:
這就涉及到 javascript 的事件迴圈機制了。
首先來複習下 async / await
, 它是 Promise
的語法糖,async
會返回一個 Promise
,而 await
則會把剩下的程式碼包裹在 then
的回撥裡,比如:
await hello() console.log(1) // 等同於 hello().then(() => { console.log(1) })
重點:await後面的程式碼相當於放在promise.then的回撥中
這裡用了 useFakeTimers
,所以 setTimeout
會替換成了 Jest 的 setTimeout
(被 Jest 接管)。當執行 jest.runAllTimers()
後,也就是執行resolve
:
const sleep = (ms: number) => { return new Promise(resolve => { setTimeout(resolve, ms); }) }
此時會把 await
後面的程式碼推入到微任務佇列中。
然後繼續執行本次宏任務中的程式碼,即expect(callback).toHaveBeenCalledTimes(1)
,這時候callback
肯定沒有執行。本次宏任務執行完後,開始執行微任務佇列中的任務,即執行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
函數會返回一個promise
,我們在promise
前面加一個await
,那麼後面的程式碼就相當於:
await promise expect(callback).toHaveBeenCalledTimes(1) 等價於 promise.then(() => { expect(callback).toHaveBeenCalledTimes(1) })
所以,這個時候就能正確的測試。
Jest 對於非同步的支援有兩種方式:回撥函數和promise
。其中回撥函數執行後,後面必須執行done
函數,表示此時測試才結束。同理,promise
的方式必須要通過return
返回。
Jest 對時間函數的支援是接管真正的時間函數,把回撥函數新增到一個陣列中,當呼叫runAllTimers()
時就執行陣列中的回撥函數。
最後通過一個典型案例,結合非同步和setTimeout
來實踐真實的測試。
以上就是詳解Jest 如何支援非同步及時間函數實現範例的詳細內容,更多關於Jest 支援非同步時間函數的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45