詳解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ù)后,測試結束。如果沒有done,同步代碼執(zhí)行完后,測試就執(zhí)行完了,測試不會等待異步代碼。
test('test callback', (done) => {
fetchUser((data) => {
expect(data).toBe('hello')
done()
})
})
需要注意的是,必須使用 done 來告訴測試用例什么時候結束,即執(zhí)行 done() 之后測試用例才結束。
promise
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')
})
Mock Timer
基本使用
假如現(xiàn)在有一個函數(shù) src/utils/after1000ms.ts,它的作用是在 1000ms 后執(zhí)行傳入的 callback:
const after1000ms = (callback) => {
console.log("準備計時");
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的進行執(zhí)行,但是我們?nèi)绻枰毩6鹊目刂疲梢允褂?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)用完的時機
jest.runOnlyPendingTimers()
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('one')
// 第二次時間函數(shù)調(diào)用
jest.runOnlyPendingTimers()
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenCalledWith('two')
})
我們還可以定義時間來控制程序的運行:
// 可以自己定義時間的前進,比如時間過去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')
})
模擬時鐘的機制
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í)行的時機也從 "1000ms 后" 變成了 Jest 執(zhí)行 "小本本" 之時 。而 Jest 提供給我們的就是執(zhí)行這個 "小本本" 的時機就是執(zhí)行runAllTimers的時機。
典型案例
學過 Java 的同學都知道 Java 有一個 sleep 方法,可以讓程序睡上個幾秒再繼續(xù)做別的。雖然 JavaScript 沒有這個函數(shù), 但我們可以利用 Promise 以及 setTimeout 來實現(xiàn)類似的效果。
const sleep = (ms: number) => {
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
理論上,我們會這么用:
console.log('開始'); // 準備
await sleep(1000); // 睡 1 秒
console.log('結束'); // 睡醒
在寫測試時,我們可以寫一個 act 內(nèi)部函數(shù)來構造這樣的使用場景:
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 沒有被調(diào)用,調(diào)用 jest.runAllTimers 后,理論上 callback 會被執(zhí)行一次。
然而,當我們跑這個用例時會發(fā)現(xiàn)最后一行的 expect(callback).toHaveBeenCalledTimes(1); 會報錯,發(fā)現(xiàn)根本沒有調(diào)用,調(diào)用次數(shù)為0:

問題分析
這就涉及到 javascript 的事件循環(huán)機制了。
首先來復習下 async / await, 它是 Promise 的語法糖,async 會返回一個 Promise,而 await 則會把剩下的代碼包裹在 then 的回調(diào)里,比如:
await hello()
console.log(1)
// 等同于
hello().then(() => {
console.log(1)
})
重點:await后面的代碼相當于放在promise.then的回調(diào)中
這里用了 useFakeTimers,所以 setTimeout 會替換成了 Jest 的 setTimeout(被 Jest 接管)。當執(zhí)行 jest.runAllTimers()后,也就是執(zhí)行resolve:
const sleep = (ms: number) => {
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
此時會把 await后面的代碼推入到微任務隊列中。
然后繼續(xù)執(zhí)行本次宏任務中的代碼,即expect(callback).toHaveBeenCalledTimes(1),這時候callback肯定沒有執(zhí)行。本次宏任務執(zhí)行完后,開始執(zhí)行微任務隊列中的任務,即執(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,那么后面的代碼就相當于:
await promise
expect(callback).toHaveBeenCalledTimes(1)
等價于
promise.then(() => {
expect(callback).toHaveBeenCalledTimes(1)
})
所以,這個時候就能正確的測試。
總結
Jest 對于異步的支持有兩種方式:回調(diào)函數(shù)和promise。其中回調(diào)函數(shù)執(zhí)行后,后面必須執(zhí)行done函數(shù),表示此時測試才結束。同理,promise的方式必須要通過return返回。
Jest 對時間函數(shù)的支持是接管真正的時間函數(shù),把回調(diào)函數(shù)添加到一個數(shù)組中,當調(diào)用runAllTimers()時就執(zhí)行數(shù)組中的回調(diào)函數(shù)。
最后通過一個典型案例,結合異步和setTimeout來實踐真實的測試。
以上就是詳解Jest 如何支持異步及時間函數(shù)實現(xiàn)示例的詳細內(nèi)容,更多關于Jest 支持異步時間函數(shù)的資料請關注腳本之家其它相關文章!
相關文章
詳解JavaScript實現(xiàn)簡單的詞法分析器示例
這篇文章主要為大家介紹了詳解JavaScript實現(xiàn)簡單的詞法分析器示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03

