51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 483|回复: 0
打印 上一主题 下一主题

异步及时间函数与测试框架Jest的故事

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2022-9-13 17:02:06 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 草帽路飞UU 于 2022-9-13 17:03 编辑

异步支持 



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

  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()

  })


可是话说回来,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);
  })
}


})


需要注意的是,必须使用 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 的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

  基本使用
  假如现在有一个函数 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())



理论上,我们会这么用:

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


  })


})





问题分析

  这就涉及到 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来实践真实的测试。

























本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏
回复

使用道具 举报

本版积分规则

关闭

站长推荐上一条 /1 下一条

小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

GMT+8, 2024-5-10 08:13 , Processed in 0.065633 second(s), 24 queries .

Powered by Discuz! X3.2

© 2001-2024 Comsenz Inc.

快速回复 返回顶部 返回列表