Jest如何支持异步及时间函数实现,能否详细解释其长尾词机制?

2026-04-01 14:132阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2187个文字,预计阅读时间需要9分钟。

Jest如何支持异步及时间函数实现,能否详细解释其长尾词机制?

目录+异步支持+回调函数+callback+Promise+Mock+Timer+基本使用+模拟时钟机制+典型案例+问题分析+解决方法+总结+异步支持+在前端开发中,我们经常会遇到大量的异步代码,那么就需要测试框架来帮助我们进行测试。

目录
  • 异步支持
    • 回调函数 callback
    • promise
  • Mock Timer
    • 基本使用
    • 模拟时钟的机制
  • 典型案例
    • 问题分析
    • 解决方法
  • 总结

    异步支持

    在前端开发中,我们会遇到很多异步代码,那么就需要测试框架对异步必须支持,那如何支持呢?

    Jest 支持异步有两种方式:回调函数及 promise(async/await)

    回调函数 callback

    const fetchUser = (cb) => { setTimeout(() => { cb('hello') }, 100) } // 必须要使用done,done表示执行done函数后,测试结束。如果没有done,同步代码执行完后,测试就执行完了,测试不会等待异步代码。 test('test callback', (done) => { fetchUser((data) => { expect(data).toBe('hello') done() }) })

    需要注意的是,必须使用 done 来告诉测试用例什么时候结束,即执行 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 的resolvesrejects表示返回的结果:

    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

    基本使用

    假如现在有一个函数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:

    Jest如何支持异步及时间函数实现,能否详细解释其长尾词机制?

    问题分析

    这就涉及到 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 支持异步时间函数的资料请关注易盾网络其它相关文章!

    本文共计2187个文字,预计阅读时间需要9分钟。

    Jest如何支持异步及时间函数实现,能否详细解释其长尾词机制?

    目录+异步支持+回调函数+callback+Promise+Mock+Timer+基本使用+模拟时钟机制+典型案例+问题分析+解决方法+总结+异步支持+在前端开发中,我们经常会遇到大量的异步代码,那么就需要测试框架来帮助我们进行测试。

    目录
    • 异步支持
      • 回调函数 callback
      • promise
    • Mock Timer
      • 基本使用
      • 模拟时钟的机制
    • 典型案例
      • 问题分析
      • 解决方法
    • 总结

      异步支持

      在前端开发中,我们会遇到很多异步代码,那么就需要测试框架对异步必须支持,那如何支持呢?

      Jest 支持异步有两种方式:回调函数及 promise(async/await)

      回调函数 callback

      const fetchUser = (cb) => { setTimeout(() => { cb('hello') }, 100) } // 必须要使用done,done表示执行done函数后,测试结束。如果没有done,同步代码执行完后,测试就执行完了,测试不会等待异步代码。 test('test callback', (done) => { fetchUser((data) => { expect(data).toBe('hello') done() }) })

      需要注意的是,必须使用 done 来告诉测试用例什么时候结束,即执行 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 的resolvesrejects表示返回的结果:

      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

      基本使用

      假如现在有一个函数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:

      Jest如何支持异步及时间函数实现,能否详细解释其长尾词机制?

      问题分析

      这就涉及到 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 支持异步时间函数的资料请关注易盾网络其它相关文章!