Jest | 跨越同步執行的 Jest 測試
前言
上一篇「讓 Jest 為你的 Code 做測試-基礎用法教學」中提到了如何使用 Jest
做單元測試,但是 JavaScript
是屬於同步執行的程式碼,這種特性會使 Jest
在測試結果出現問題,本篇會針對這點來講解關於異步測試的方法。
異步測試
測試流程
首先建立一個用 setTimeout
的延遲模擬請求獲取資料的 function
,獲取後再將資料傳到 callBack
中執行。
這裡將函式 fetchData
放在 ./func/async.js
中:
//傳入一個 callBack 函數,在獲取資料時執行
const fetchData = (callBack) => {
setTimeout(() => {
callBack('getData')
}, 3000)
}
//將該 fetchData 函式匯出
module.exports = {
fetchData: fetchData
}
建立測試檔案 ./test/async.test.js
,在檔案中匯入 fetchData
,並在 callBack
函式內設定斷言,測試 fetchData
回傳的資料是否符合期望值:
let async = require('../funcs/async.js')
//建立測試
test('test async', () => {]
//callBack 會在 fetchData 取得資料後執行
const callBack = (data) => {
expect(data).toBe('getData')
}
//將上方的 callBack 函式傳入 fetchData 中
async.fetchData(callBack)
})
完成以上設置後,便可以執行測試了,這裡使用能夠產生覆蓋率報告的測試指令,會得到以下結果:
可以看到結果是 PASS
,也就是說 callBack
接收到的斷言和我們期望的值相同,但是卻發現測試的覆蓋率卻不是 100 %,代表在 fetchData
中有些地方沒有執行到,測試就已經結束了,這時可以點開 ./coverage/Icov-report
內關於 async.js
的執行報告來看:
發現問題
經過上方的操作,可以發現就算在測試中 JavaScript
也是一如既往同步執行,不會等到 callBack
執行,也不會到設定的斷言,整個測試就已經結束了。
這裡可以將代碼改為更直接的方式測試:
let async = require('../funcs/async.js')
test('test async', () => {
const callBack = (data) => {
//將斷言中的結果傳入空值,期望值為 'getData' 不變
expect('').toBe('getData')
}
async.fetchData(callBack)
})
有興趣可以試試上方的測試,結果依然會是 PASS
,因為 callBack
函式根本就沒有執行到。
解決問題
雖然在 JavaScript
中,處理同步問題一直不是輕鬆的事情,但是 Jest
在執行測試的時候可以透過 done()
來應付這個狀況。
簡單來說,如果在測試裡有加入 done()
,那只要還沒執行到 done()
就不算結束測試,因此可以將它加入上方的程式裡:
let async = require('../funcs/async.js')
//將 done 傳入測試中
test('test async', (done) => {
const callBack = (data) => {
expect(data).toBe('getData')
//在 callBack 函式內的斷言後加上 done
done()
}
async.fetchData(callBack)
})
加入 done()
後重新執行測試,就可以看到報告呈現了完美的一片綠光。
需要注意的是,如果測試中有傳入 done
但卻未執行它,那麼該測試結果就會出現 FAIL
失敗:
let async = require('../funcs/async.js')
test('test async', (done) => {
const callBack = (data) => {
expect(data).toBe('getData')
}
async.fetchData(callBack)
})
執行測試結果如下:
使用 Promise
如果既有的程式碼已使用 Promise
處理同步,便不必再使用 done
,直接以 .then
接收 Promise
物件傳進 resolve
的結果即可:
const promiseFetchData = () =>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('getData')
}, 3000)
})
}
module.exports = {
promiseFetchData: promiseFetchData
}
let async = require('../funcs/async.js')
test('test promise async', ()=>{
//return 是必須的,否則不會執行 .then 的內容
return async.promiseFetchData()
.then((data)=>{
expect(data).toBe('getData')
})
})
需要注意的是,在測試的 function
中必須加上 return
,否則測試不會跑進 .then
中,只會在 Promise
將結果送進 resolve
時就結束了,另外!當 Promise
中的結果跑進 reject
那測試也會產生錯誤。
測試結果得到 PASS
,覆蓋率也是 100 %:
上述說明了成功的 resolve
在測試中用 .then
接收,而失敗的 reject
則是使用 .catch
處理接下來的動作,這部分和 Promise
的操作都相同,就不再闡述,如果對 Promise
不熟,可以參考「JacaScript | 從Promise開始承諾的部落格生活」。
Jest
在 expect
內還另外擁有兩個內建屬性來針對 Promise
做處理,分別為 resolves
及 rejects
,他們會直接捕捉 Promise
傳進resolve
或 reject
的資料判斷是否符合期望值,這裡以 rejects
作為例子:
const promiseErrorFetchData = () =>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('error')
}, 3000)
})
}
module.exports = {
promiseErrorFetchData: promiseErrorFetchData
}
測試時,直接將 promiseErrorFetchData
作為參數傳遞給 expect
,並透過 expect
的內建屬性 rejects
取得 promiseErrorFetchData
中 Promise
傳進 reject
的資料下斷言:
test('test rejects in promise',()=>{
//return 是必須的,否則不會等 Promise 執行到 reject 測試就結束了
return expect(async.promiseErrorFetchData()).rejects.toBe('error')
})
這個方式筆者認為能夠更直覺性的使用 Promise
的測試,得到的結果也都和上方一樣,但主要還是配合團隊使用其中一種,才不會讓測試代碼顯得雜亂,當然!個人開發也是一樣。
本文針對異步請求在測試內產生的問題舉了幾個例子解決,但其實官網上還有提出一種使用 async/await
的方式,不過筆者還未學習到 JavaScript
中關於 async
及 await
的用法,因此怕現階段會誤導大家,就等日後使用到再回來補充文章內容。
如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!
參考文章