欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Jest?如何支持異步及時(shí)間函數(shù)實(shí)現(xiàn)示例

 更新時(shí)間:2022年09月06日 14:05:15   作者:小p  
這篇文章主要為大家介紹了Jest?如何支持異步及時(shí)間函數(shù)實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

異步支持

在前端開(kāi)發(fā)中,我們會(huì)遇到很多異步代碼,那么就需要測(cè)試框架對(duì)異步必須支持,那如何支持呢?

Jest 支持異步有兩種方式:回調(diào)函數(shù)及 promise(async/await)。

回調(diào)函數(shù) callback

const fetchUser = (cb) => {
  setTimeout(() => {
      cb('hello')
  }, 100)
}
// 必須要使用done,done表示執(zhí)行done函數(shù)后,測(cè)試結(jié)束。如果沒(méi)有done,同步代碼執(zhí)行完后,測(cè)試就執(zhí)行完了,測(cè)試不會(huì)等待異步代碼。
test('test callback', (done) => {
  fetchUser((data) => {
      expect(data).toBe('hello')
      done()
  })
})

需要注意的是,必須使用 done 來(lái)告訴測(cè)試用例什么時(shí)候結(jié)束,即執(zhí)行 done() 之后測(cè)試用例才結(jié)束。

promise

const userPromise = () => Promise.resolve('hello')
test('test promise', () => {
  // 必須要用return返回出去,否則測(cè)試會(huì)提早結(jié)束,也不會(huì)進(jìn)入到異步代碼里面進(jìn)行測(cè)試
  return userPromise().then(data => {
    expect(data).toBe('hello')
  })
})
// async
test('test async', async () => {
  const data = await userPromise()
  expect(data).toBe('hello')
})

針對(duì) promise,Jest 框架提供了一種簡(jiǎn)化的寫(xiě)法,即 expect 的resolvesrejects表示返回的結(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)在有一個(gè)函數(shù) src/utils/after1000ms.ts,它的作用是在 1000ms 后執(zhí)行傳入的 callback

const after1000ms = (callback) => {
  console.log("準(zhǔn)備計(jì)時(shí)");
  setTimeout(() => {
    console.log("午時(shí)已到");
    callback && callback();
  }, 1000);
};

如果不 Mock 時(shí)間,那么我們就得寫(xiě)這樣的用例:

describe("after1000ms", () => {
  it("可以在 1000ms 后自動(dòng)執(zhí)行函數(shù)", (done) => {
    after1000ms(() => {
      expect(...);
      done();
    });
  });
});

這樣我們得死等 1000 毫秒才能跑這完這個(gè)用例,這非常不合理,現(xiàn)在來(lái)看看官方的解決方法:

const fetchUser = (cb) => {
  setTimeout(() => {
      cb('hello')
  }, 1000)
}
// jest用來(lái)接管所有的時(shí)間函數(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)用了,因?yàn)楸籮est接管了
  expect(setTimeout).toHaveBeenCalledTimes(1)
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000)
  // 跑完所有的時(shí)間函數(shù)
  jest.runAllTimers()
  expect(callback).toHaveBeenCalled()
  expect(callback).toHaveBeenCalledWith('hello')
})

runAllTimers是對(duì)所有的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í)間函數(shù)調(diào)用完的時(shí)機(jī)
  jest.runOnlyPendingTimers()
  expect(callback).toHaveBeenCalledTimes(1)
  expect(callback).toHaveBeenCalledWith('one')
  // 第二次時(shí)間函數(shù)調(diào)用
  jest.runOnlyPendingTimers()
  expect(callback).toHaveBeenCalledTimes(2)
  expect(callback).toHaveBeenCalledWith('two')
})

我們還可以定義時(shí)間來(lái)控制程序的運(yùn)行:

// 可以自己定義時(shí)間的前進(jìn),比如時(shí)間過(guò)去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')
})

模擬時(shí)鐘的機(jī)制

Jest 是如何模擬 setTimeout 等時(shí)間函數(shù)的呢?

我們從上面這個(gè)用例多少能猜得出:Jest  "好像"  用了一個(gè)數(shù)組記錄 callback,然后在 jest.runAllTimers 時(shí)把數(shù)組里的 callback 都執(zhí)行, 偽代碼可能是這樣的:

setTimeout(callback) // Mock 的背后 -> callbackList.push(callback)
jest.runAllTimers() // 執(zhí)行 -> callbackList.forEach(callback => callback())

可是話說(shuō)回來(lái),setTimeout 本質(zhì)上不也是用一個(gè) "小本本" 記錄這些 callback,然后在 1000ms 后執(zhí)行的么?

那么,我們可以提出這樣一個(gè)猜想:調(diào)用 jest.useFakeTimers 時(shí),setTimeout 并沒(méi)有把 callback 記錄到 setTimeout 的 "小本本" 上,而是記在了 Jest 的 "小本本" 上!

所以,callback 執(zhí)行的時(shí)機(jī)也從  "1000ms 后"  變成了  Jest 執(zhí)行 "小本本" 之時(shí) 。而 Jest 提供給我們的就是執(zhí)行這個(gè) "小本本" 的時(shí)機(jī)就是執(zhí)行runAllTimers的時(shí)機(jī)。

典型案例

學(xué)過(guò) Java 的同學(xué)都知道 Java 有一個(gè) sleep 方法,可以讓程序睡上個(gè)幾秒再繼續(xù)做別的。雖然 JavaScript 沒(méi)有這個(gè)函數(shù), 但我們可以利用 Promise 以及 setTimeout 來(lái)實(shí)現(xiàn)類(lèi)似的效果。

const sleep = (ms: number) => {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  })
}

理論上,我們會(huì)這么用:

console.log('開(kāi)始'); // 準(zhǔn)備
await sleep(1000); // 睡 1 秒
console.log('結(jié)束'); // 睡醒

在寫(xiě)測(cè)試時(shí),我們可以寫(xiě)一個(gè) act 內(nèi)部函數(shù)來(lái)構(gòu)造這樣的使用場(chǎng)景:

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);
  })
})

上面的用例很簡(jiǎn)單:在 "快進(jìn)時(shí)間" 之前檢查 callback 沒(méi)有被調(diào)用,調(diào)用 jest.runAllTimers 后,理論上 callback 會(huì)被執(zhí)行一次。

然而,當(dāng)我們跑這個(gè)用例時(shí)會(huì)發(fā)現(xiàn)最后一行的 expect(callback).toHaveBeenCalledTimes(1); 會(huì)報(bào)錯(cuò),發(fā)現(xiàn)根本沒(méi)有調(diào)用,調(diào)用次數(shù)為0:

問(wèn)題分析

這就涉及到 javascript 的事件循環(huán)機(jī)制了。

首先來(lái)復(fù)習(xí)下 async / await, 它是 Promise 的語(yǔ)法糖,async 會(huì)返回一個(gè) Promise,而 await 則會(huì)把剩下的代碼包裹在 then 的回調(diào)里,比如:

await hello()
console.log(1)
// 等同于
hello().then(() => {
  console.log(1)
})

重點(diǎn):await后面的代碼相當(dāng)于放在promise.then的回調(diào)中

這里用了 useFakeTimers,所以 setTimeout 會(huì)替換成了 Jest 的 setTimeout(被 Jest 接管)。當(dāng)執(zhí)行 jest.runAllTimers()后,也就是執(zhí)行resolve

const sleep = (ms: number) => {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  })
}

此時(shí)會(huì)把 await后面的代碼推入到微任務(wù)隊(duì)列中。

然后繼續(xù)執(zhí)行本次宏任務(wù)中的代碼,即expect(callback).toHaveBeenCalledTimes(1),這時(shí)候callback肯定沒(méi)有執(zhí)行。本次宏任務(wù)執(zhí)行完后,開(kāi)始執(zhí)行微任務(wù)隊(duì)列中的任務(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ù)會(huì)返回一個(gè)promise,我們?cè)?code>promise前面加一個(gè)await,那么后面的代碼就相當(dāng)于:

await promise
expect(callback).toHaveBeenCalledTimes(1)
等價(jià)于
promise.then(() => {
    expect(callback).toHaveBeenCalledTimes(1)
})

所以,這個(gè)時(shí)候就能正確的測(cè)試。

總結(jié)

Jest 對(duì)于異步的支持有兩種方式:回調(diào)函數(shù)和promise。其中回調(diào)函數(shù)執(zhí)行后,后面必須執(zhí)行done函數(shù),表示此時(shí)測(cè)試才結(jié)束。同理,promise的方式必須要通過(guò)return返回。

Jest 對(duì)時(shí)間函數(shù)的支持是接管真正的時(shí)間函數(shù),把回調(diào)函數(shù)添加到一個(gè)數(shù)組中,當(dāng)調(diào)用runAllTimers()時(shí)就執(zhí)行數(shù)組中的回調(diào)函數(shù)。

最后通過(guò)一個(gè)典型案例,結(jié)合異步和setTimeout來(lái)實(shí)踐真實(shí)的測(cè)試。

以上就是詳解Jest 如何支持異步及時(shí)間函數(shù)實(shí)現(xiàn)示例的詳細(xì)內(nèi)容,更多關(guān)于Jest 支持異步時(shí)間函數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • JavaScript利用html5新方法操作元素類(lèi)名詳解

    JavaScript利用html5新方法操作元素類(lèi)名詳解

    這篇文章主要給大家分享的是JavaScript用html5新方法操作元素類(lèi)名的詳解,早先JavaScript處理起來(lái)特別不方便,需要先取到class屬性,然后對(duì)字符串進(jìn)行處理。現(xiàn)在html5給所有元素增加了classList屬性來(lái)操作類(lèi)屬性,非常方便,下面就一起來(lái)看看具體操作過(guò)程吧
    2021-11-11
  • JavaScript復(fù)原何同學(xué)B站頭圖細(xì)節(jié)示例詳解

    JavaScript復(fù)原何同學(xué)B站頭圖細(xì)節(jié)示例詳解

    這篇文章主要為大家介紹了JavaScript復(fù)原何同學(xué)B站頭圖細(xì)節(jié)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • 非常好的js代碼

    非常好的js代碼

    [藍(lán)色]非常好的js代碼...
    2006-06-06
  • JavaScript兩張圖搞懂原型鏈

    JavaScript兩張圖搞懂原型鏈

    這篇文章主要分享JavaScript的兩張?jiān)玩湀D,我們上一篇文章介紹了JavaScript中的原型,為什么不將原型鏈一起介紹了呢?因?yàn)镴avaScript中的原型鏈?zhǔn)且粋€(gè)難點(diǎn),也是一個(gè)面試必問(wèn)的考點(diǎn),現(xiàn)在就來(lái)學(xué)習(xí)一下具體內(nèi)容吧
    2021-12-12
  • js前端圖片加載異常兜底方案

    js前端圖片加載異常兜底方案

    這篇文章主要為大家介紹了js前端圖片加載異常兜底方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • JavaScript中Map與Object應(yīng)用場(chǎng)景

    JavaScript中Map與Object應(yīng)用場(chǎng)景

    這篇文章主要為大家介紹了JavaScript中Map與Object應(yīng)用場(chǎng)景的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • 微信小程序本作用域下調(diào)用全局JS詳解及實(shí)例

    微信小程序本作用域下調(diào)用全局JS詳解及實(shí)例

    這篇文章主要介紹了微信小程序本作用域下調(diào)用全局JS詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下
    2017-02-02
  • JavaScript深拷貝方法structuredClone使用

    JavaScript深拷貝方法structuredClone使用

    這篇文章主要為大家介紹了JavaScript深拷貝方法structuredClone使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-02-02
  • 微信小程序 實(shí)戰(zhàn)實(shí)例開(kāi)發(fā)流程詳細(xì)介紹

    微信小程序 實(shí)戰(zhàn)實(shí)例開(kāi)發(fā)流程詳細(xì)介紹

    這篇文章主要介紹了微信小程序 實(shí)戰(zhàn)實(shí)例開(kāi)發(fā)流程詳細(xì)介紹的相關(guān)資料,這里主要介紹微信小程序的開(kāi)發(fā)流程和簡(jiǎn)單實(shí)例,需要的朋友可以參考下
    2017-01-01
  • 微信小程序的生命周期的詳解

    微信小程序的生命周期的詳解

    這篇文章主要介紹了微信小程序的生命周期的詳解的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下
    2017-10-10

最新評(píng)論