详解Jest 如何支持异步及时间函数实现示例

目录
  • 异步支持
    • 回调函数 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:

问题分析

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

(0)

相关推荐

  • 基于Spring Data Jest的Elasticsearch数据统计示例

    命令查询职责分离模式(Command Query Responsibility Segregation,CQRS)从业务上分离修改 (Command,增,删,改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为.从而使得逻辑更加清晰,便于对不同部分进行针对性的优化. CQRS有以下几点有点: 1.分工明确,可以负责不同的部分: 2.将业务上的命令和查询的职责分离能够提高系统的性能.可扩展性和安全性.并且在系统的演化中能够保持高度的灵活性,能够防止出现CRUD模式中,对

  • Vue-Jest 自动化测试基础配置详解

    目录 安装 配置 常见错误 测试前的工作 处理依赖 生成实例和 DOM 总结 引用 目前开发大型应用,测试是一个非常重要的环节,而在 Vue 项目中做单元测试可以用 Jest,Jest 是 facebook 推出的一款测试框架,集成了 Mocha, chai, jsdom, sinon 等功能,而且在 Vue 的脚手架中已经集成了 Jest,所以在 Vue 项目中使用 Jest 做单元测试是不二的选择,从提供的例子上看都很简单地配置并测试成功,然而在实际项目中有很多差异,我在测试自己的某个业务组

  • SpringBoot 整合Jest实例代码讲解

    [1]添加Elasticsearch-starter pom文件添加starter如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> SpringBoot默认支持两种技术和Elasticsearch进行交互:Sp

  • 详解Jest结合Vue-test-utils使用的初步实践

    介绍 Vue-test-utils是Vue的官方的单元测试框架,它提供了一系列非常方便的工具,使我们更加轻松的为Vue构建的应用来编写单元测试.主流的 JavaScript 测试运行器有很多,但 Vue Test Utils 都能够支持.它是测试运行器无关的. Jest,是由Facebook开发的单元测试框架,也是Vue推荐的测试运行器之一.Vue对它的评价是: Jest 是功能最全的测试运行器.它所需的配置是最少的,默认安装了 JSDOM,内置断言且命令行的用户体验非常好.不过你需要一个能够将

  • 详解使用jest对vue项目进行单元测试

    最近领导对前端提出了新的要求,要进行单元测试.之前使用vue做了一个快报名小程序的pc端页面,既然要做单元测试,就准备用这个项目了,之前有些react的经验,vue还是第一遭 vue-cli3.0单元测试方面更加完备,就先升级到了cli3.0,因为项目是用typescript写的,需要ts-jest,得到jest的配置如下 { "jest": { "moduleFileExtensions": [ "js", "jsx", &

  • 使用 Jest 和 Supertest 进行接口端点测试实例详解

    本文实例讲述了使用 Jest 和 Supertest 进行接口端点测试.分享给大家供大家参考,具体如下: 如何创建测试是一件困难的事.网络上有许多关于测试的文章,却从来不告诉你他们是如何开始创建测试的. 所以,今天我将分享我在实际工作中是如何从头开始创建测试的.希望能够对你提供一些灵感. 目录: 使用 Express 创建一个应用 使用 Mongoose 链接 MongoDB 使用 Jest 作为测试框架 为什么使用 Jest 易于使用 wath-mode 非常棒 开始使用 Jest 首先,你需

  • 详解Jest 如何支持异步及时间函数实现示例

    目录 异步支持 回调函数 callback promise Mock Timer 基本使用 模拟时钟的机制 典型案例 问题分析 解决方法 总结 异步支持 在前端开发中,我们会遇到很多异步代码,那么就需要测试框架对异步必须支持,那如何支持呢? Jest 支持异步有两种方式:回调函数及 promise(async/await). 回调函数 callback const fetchUser = (cb) => { setTimeout(() => { cb('hello') }, 100) } //

  • 详解vue-router的Import异步加载模块问题的解决方案

    1.问题现象 2.出现问题的代码点 3.替代方案: 把import() 替换成如下: Promise.resolve().then(()=>require(`@/views/${str}`)) 4.原因分析 项目在编译时,出现一个警告 这个警告的含义: require接收了一个变量,会报上面的警告,接收一个写死的字符串值时则没有警告! 我们通过控制台查看到import()对应编译过后的代码: 从上图可以看到require接收了一个变量,所以运行时出现了警告. 那这样就会报上面找不到对应的模块.

  • 详解nodejs中的异步迭代器

    前言 从 Node.jsv10.0.0 开始,异步迭代器就出现中了,最近它们在社区中的吸引力越来越大.在本文中,我们将讨论异步迭代器的作用,还将解决它们可能用于什么目的的问题. 什么是异步迭代器 那么什么是异步迭代器?它们实际上是以前可用的迭代器的异步版本.当我们不知道迭代的值和最终状态时,可以使用异步迭代器,最终我们得到可以解决{value:any,done:boolean}对象的 promise.我们还获得了 for-await-of 循环,以帮助我们循环异步迭代器.就像 for-of 循环

  • 详解Java中CountDownLatch异步转同步工具类

    使用场景 由于公司业务需求,需要对接socket.MQTT等消息队列. 众所周知 socket 是双向通信,socket的回复是人为定义的,客户端推送消息给服务端,服务端的回复是两条线.无法像http请求有回复. 下发指令给硬件时,需要校验此次数据下发是否成功. 用户体验而言,点击按钮就要知道此次的下发成功或失败. 如上图模型, 第一种方案使用Tread.sleep 优点:占用资源小,放弃当前cpu资源 缺点: 回复速度快,休眠时间过长,仍然需要等待休眠结束才能返回,响应速度是固定的,无法及时响

  • 详解Python实现多进程异步事件驱动引擎

    本文介绍了详解Python实现多进程异步事件驱动引擎,分享给大家,具体如下: 多进程异步事件驱动逻辑 逻辑 code # -*- coding: utf-8 -*- ''' author: Jimmy contact: 234390130@qq.com file: eventEngine.py time: 2017/8/25 上午10:06 description: 多进程异步事件驱动引擎 ''' __author__ = 'Jimmy' from multiprocessing import

  • 详解在java中进行日期时间比较的4种方法

    1. Date.compareTo() java.util.Date提供了在Java中比较两个日期的经典方法compareTo(). 如果两个日期相等,则返回值为0. 如果Date在date参数之后,则返回值大于0. 如果Date在date参数之前,则返回值小于0. @Test void testDateCompare() throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

  • 详解Python常用标准库之时间模块time和datetime

    目录 time时间模块 time -- 获取本地时间戳 localtime -- 获取本地时间元组(UTC) gmtime -- 获取时间元组(GMT) mktime -- 时间元组获取时间戳 ctime -- 获取时间字符串 asctime -- 时间元组获取时间字符串 strftime -- 格式化时间 strptime -- 格式化时间 sleep -- 时间睡眠 perf_counter -- 时间计时 模拟进度条 程序计时 时间转换示意图 datetime时间模块 date类 time

  • 详解vite如何支持cjs方案示例

    目录 一.问题 二.解决方案 三.如何处理commonJS 一.问题 vite运行时使用esbuild,基于esm 大部分三方包为UMD规范,输出的是CommonJS的包(比如react.lodash) // react 入口文件 // 只有 CommonJS 格式 if (process.env.NODE_ENV === "production") { module.exports = require("./cjs/react.production.min.js"

  • 详解Node.js使用token进行认证的简单示例

    本文只介绍简单的应用,关于json web token的具体介绍以及原理请参考阮一峰老师的JSON Web Token 入门教程. 使用的Node框架是koa2,前端发送ajax请求使用axios 首先创建工程目录: static中存放静态资源,views存放前端模板,server.js为后端代码. 安装必要的依赖项: "dependencies": { "@koa/router": "^8.0.8", "jsonwebtoken&qu

  • 详解C++调用Python脚本中的函数的实例代码

    1.环境配置 安装完python后,把python的include和lib拷贝到自己的工程目录下 然后在工程中包括进去 2.例子 先写一个python的测试脚本,如下 这个脚本里面定义了两个函数Hello()和_add().我的脚本的文件名叫mytest.py C++代码: #include "stdafx.h" #include <stdlib.h> #include <iostream> #include "include\Python.h&quo

随机推荐