Jest 测试框架入门笔记

914 阅读23分钟

1. Jest 测试框架介绍

image.png

Jest 由 meta 也就是 Facebook 维护。能用于测试 node, js, typescript, react 或 vue 等项目。

image.png

1.1 测试的定义及种类

那么什么是测试呢?测试的意思就是检查代码的执行是否符合预期,我们程序员在编写代码的时候必然会出现错误,通过测试,我们可以在早期就发现可能的错误和缺陷,保证程序正常的运行,后期在修改代码时确保结果仍然符合预期。

测试在大体上可以分为:

  • 单元测试,是最简单的测试,经常用于测试单个函数,例如一个组件,给它特定的参数能够返回特定的结果。
  • 集成测试,比单元测试更复杂,需要处理依赖关系。例如某个功能涉及多个函数的调用,需要统一进行测试。
  • 端到端测试,测试程序和用户交互的流程,例如输入文本得到预期的结果。
  • UI测试,是前端重点的测试,用于测试页面视图显示正常,功能正确,并且性能符合预期,多数情况下属于人工测试

如果项目是使用 JavaScript 编写的,那么使用 Jest 测试框架能够帮我们实现自动化的测试,不用再人工或者手动编写 console.log 来测试了,只需要运行一条命令就能够跑完所有测试,节省时间和人力。

1.2 为什么是 Jest

常用的测试框架有 Mocha Jasmine Jest 等。这些测试框架都具有以下的基本功能:

  1. 断言
  2. 异步支持
  3. Mock功能
  4. 代码重复率检查

而 Jest 的优势在于:

  • 开箱即用,零配置
  • 运行速度快
  • 内置检查代码重复率的共嫩南瓜
  • Mock 非常容易使用

2. Jest 框架的基本用法

接下来我们看一下 Jest 的框架本身的用法,这里我已经简单地创建好了一个 NodeJs 项目,要使用 js 框架就需要先把它添加到依赖里边,运行 yarn add --dev jest,把这个 jest 添加到项目里边。

安装 Jest 的时候,把它添加到开发者依赖就可以了,因为测试是在开发的时期进行的

项目结构如下所示:

image.png

2.1 toBe 方法

我们先来看一下它最简单的使用方法,这里边有一个 sum.js 它里边导出了一个 sum 函数,接收两个参数 ab ,然后返回这两个参数的和。这样很简单的一个加法函数,我们要测试它的功能正常,那么我们编写 jest 测试代码,首先创建一个和它同名的文件,但是在它后边加上 .test 扩展名。创建一个 sum.test.js 这样一个文件。那么 Jest 的框架就会自动识别这个测试文件,里边可以编写测试代码。

首先导入 sum 函数,使用 const sum = require('./sum.js);,这里是 NodeJs 项目,所以需要使用 require。之后,使用一个 test 函数,这个函数是 Jest 提供的全局可用的 API 用于专门编写测试代码的,它第一个参数接收测试的描述信息,方便我们开发者来观看

这里的参数 1 取值为 "1+2=3",第 2 个参数就是我们要编写的测试代码了,直接给它传递一个没有参数的回调函数,在回调函数里边编写测试代码,比如说我们要保证调用 sum 之后给它传递 12,它的结果是 3,那么我们这里使用一个 expect 函数,然后在里边调用 sum 函数给它传递 12,这样的话调用 expect 可以访问它里边的一些比对方法,例如说这里调用它的 tobe 就可以判断结果是不是等于一个值。

image.png

到此这样就完成了第一个比较简单的测试代码。

接下来我们需要运行 Jest 的命令,帮我们跑一下这个测试。需要先在 package.json 里边添加一个命令来运行 Jest,在 package.json 文件里边添加一个 scripts 配置项,在里边给它添加一个 test 命令,然后运行 Jest 框架所提供的命令。

image.png

在命令行里边运行 yarn test,可以看到它提示 test 有 1 passed,就是通过了一项测试。

image.png

我们只编写了一个测试,可以看到它这是 pass 状态是绿色的,这样它就保证了 sum 函数运行符合了预期。

假如说我们后续在修改代码的时候,不小心把 sum 函数代码改错了,比如说返回了 a-b 。 这样的话我们再运行一下 yarn test ,可以看到它这个就提示了 1 failed 有一个失败了,测试没有通过,也能够看到具体的是没有通过的地方,也就是 sum(1, 2) 它不等于 3 ,然后这样的话我们就知道是哪里出了问题就方便我们定位,如果这个函数写在了一个比较深的地方

2.2 toEqual 方法

expect 后边它可以调用的一些判断方法,这里边的 tobe 是严格相等,对于普通的数字类型或者字符串类型可以这样对比,如果是对象或者是数组类型的话,使用 tobe 就不可以了。

文件 addToArray.js 它里边就是接收了一个数组作为参数,然后边接收多个参数,用于把后边的这些参数添加到前面的数组里边,里边就简单的调用了数组中的铺式方法。我们来测试一下这个函数。首先新建一个 addToArray.test.js 文件。在里边我们还是使用 test 函数,稍后我们利用自动导入来把 addToArray 函数导入进来。

例如这个测试的描述信息叫做 "添加4 5 6到数组[1,2,3]中",第二个参数传递一个回调函数,在里边我们定义一个原始的数组,const arr = [1,2,3];,然后调用 addToArray 这个函数并把 arr 传递给它,后边给它添加 4, 5, 6 这 3 个参数,接下来我们还是使用 expect 这个函数把 arr 传递进去,判断它如果我们调用 tobe 是不是等于 [1, 2, 3, 4, 5, 6] 这个字面值的数组?

image.png

写完之后我们跑一下测试,试一下运行 yarn test,可以看到有一个 failed 1 个 passed,这个 passed 是之前的 sum 里面的 test,这个 failed 就是可以看到这里 arr 使用 tobe 判断是不是等于后边的 [1, 2, 3, 4, 5, 6] 这里就出错了!

因为 tobe 是严格相等,[1, 2, 3, 4, 5, 6] 数组的引用和之前的原数组引用是不一样的,所以它这里就不能使用 tobe 要判断它里边的内容是不是相同的话,需要使用 toEqual。这样的话它比对的就是里边的每一个元素了。

修改成 toEqual 我们再来跑一下,可以看到这两个测试就都通过了。

2.3 not 方法

expect 里边它还有一个 .not 用于否定来判断,例如说我们把这个 toEqual 改成 tobe ,加上了前面的 .not,它就是等于不严格相等于后边的 [1, 2, 3, 4, 5, 6] 数组,这样的话它也是可以通过的,不过这样的话就没有什么意义了。可以看到它现在两个测试都能通过,这个是 not 的用法

expect 后边还有好多用于比对的方法,可以查看官方的 API 文档来测试一下。总之,expect 的函数就是用于执行它参数里边的 js 表达式,然后他会用一系列方法来比对返回结果和预期的值,如果满足条件的话就测试通过,不满足条件就测试失败。

2.4 Mock 函数

有时候我们可能需要测试一个函数是不是正常运行,但是不关心这个函数里边具体的实现代码,例如说测试一个回调函数,那么 Jest 支持我们创建一个 Mock 函数,然后它创建出来的 Mock 函数可以测试它的一些参数,还有返回结果以及调用次数等等这些信息。

利用 Mock 函数,我们也可以测试一些数据,比如说这里我有一个 map.js 文件,里边导出了一个 map 函数,它就是简单的包装了一下数组里边的 map,这个方法它接收一个参数,第一个参数是一个数组,然后第二个参数是一个回调函数,在里边把这个回调函数传递给数组的 map 方法。

现在测试一下回调函数是不是成功的运行了,看一下它的测试怎么写。

首先新建一个 map.test.js 文件,假如说我们要 map 一个数组 [1, 2, 3],然后判断它的回调函数是不是正常的调用了三次,那么我们可以这样写使用一个 test 函数,然后写上 "map [1, 2, 3]这个数组,回调函数",执行 3 次这样一个描述信息,然后再回调函数里边编写测试代码,这里我们需要创建一个 Mock 的回调函数,可以直接使用 jest.fn 创建它。

回调函数的内容为 x*2,每个数字元素进行乘 2 ,然后把它的返回结果保存到一个常量里边,叫做 mockFn。然后调用之前定义好的 map 函数,这里边自动导入进来,然后给它传递一个数组 [1, 2, 3],第二个参数传递 mockFn 作为回调函数,这个 mockFn有额外的一些属性方便测试,如果直接传递这个普通的 js 函数的话,我们是无法知道它的一些调用方式以及参数或者返回结果的,这样我们调用完之后,使用 expect 来判断 Mock 函数是不是执行了 3 次。

可以通过 mockFn.mock.calls.length 来访问它执行了多少次,之后调用 tobe(3) 来判断它是不是跑了 3 次。好,这样的话咱们的测试代码就写完了,运行一下,yarn test

可以看到这些测试项都通过了,包括我们刚写的回调函数执行了 3 次,我们也可以测试一下回调函数返回的结果。

image.png

现在开始测试 "map(123),回调函数返回2、4和6",因为对于每一次函数的调用都是对数组的元素进行乘 2 的操作,这样的话前边创建 Mock 函数以及给 map 传递 Mock 函数的方式不变,后边比对的是它的返回结果,那么我们可以通过 mockFn.mock.results 来访问它的执行结果,这个 results 是一个数组,包括 Mock 的回调函数的所有执行结果,我们访问 [0] 它就是第一次运行时返回的结果,然后访问它的 value 就能够得到它真实的值。

将上述代码的 tobe 改成 2 ,判断它第一次运行是不是返回了 2 ,然后复制粘贴两次访问result[1],判断它是不是等于 4,最后访问 result[2],判断它是不是等于 6。

image.png

再运行一下测试 yarn test 可以看到这些测试也都通过了。

2.5 Test Suites 的含义

注意一下 Test Suites 是 3,表示 test 写在了 3 个文件里边,而 test 包含 4 个测试,因为 map.test.js 里边包含 2 个 test,这样的话总计一共有 4 个测试,但是分散在 3 个文件里,Test Suites 就是 3。

2.6 describe 函数的测试分组

可以在一个 Test Suites 里边,即一个测试文件里边使用 describe 函数来对相关的 test 进行分组。通过多次调用 describe 函数,把相关的 test 都放在不同的分组里边

describe 函数,接收的参数和 test 是一样的,第一个参数就是描述一下测试分组的信息,例如说叫做"测试 map回调函数执行情况",然后第二个参数也是一个回调函数在里边,把相关的 test 放进来就行了。

比如将 map.test.js 中的两个 test 都放进来,都是用于测试 map 回调函数的,这样就把它放到了一个分组里边,其他的也可以再多定义几个 describe 来放到不同的分组,这样做了之后,测试结果是不变的。

2.7 coverage 测试覆盖率

最后来看一下 coverage ,对于测试来说,代码的覆盖率也是一个重要的指标,通常情况下要达到 100%,也就是对所有的代码都进行了测试。我们可以这里在 package.json 里边 Jest 的命令,后边加上 --coverage,来让它自动帮我们生成这个覆盖率的统计数据。

image.png

运行一下 yarn test

image.png

好,在命令行里边我们可以看到这个覆盖率情况,可以看到对于所有的文件它的代码我们都测试到了,都是 100% ,而 Jest 也会生成一个 coverage,目录它里边包含网页版的覆盖率的情况,打开 index.html 我们可以运行一下它,使用 live server 看一下结果,可以看到这是一个带有 UI 样式的覆盖率表格。

image.png

从结果可以看到对于每个文件里边测试所覆盖的情况

接下来看一下没有 100% 覆盖是什么情况。例如说我们把 sum.test.js 改一下,expect 里边不调用 sum 函数,而是直接给它传递一个 js 表达式 1+2 ,如下图所示:

image.png

然后运行一下 yarn test,可以看到这个覆盖率就不是 100% 了,因为我们没有测试 sum 这个函数。

image.png

好了,以上就是 Jest 测试的一些相关概念,以及 Jest 框架本身的一些用法,你学会了吗?如果有帮助请点个赞吧,可以在评论区留言,感谢阅读。

3. 异步测试

3.1 使用 done 函数明确测试结束的时间

看下面的测试代码,由于测试的网络请求是一个异步的过程,因此,Jest 在进行测试的时候不会等待,或者不知道要等待异步过程;所以 Jest 将同步代码执行完毕之后就结束了,这个时候无法达到测试目的,不论怎样测试都是通过的:

it('test callback', ()> {
  fetchUser(data => {
    expect(data).toBe('hello');
  })
})

这个时候需要明确的告诉 Jest 什么时候结束,如下所示:

it('test callback', (done)> {
  fetchUser(data => {
    expect(data).toBe('hello');
    done();
  })
})

3.2 将 Promise 放到 return 中

有了上面的经验不难理解,如果测试的对象是 Promise ,也会因为不同步的问题造成无法达到测试目的的情况,所以测试 Promise 的时候,需要将其放在 return 中:

const userPromise = () => Promise.resolve('hello');
it('test Promise', () => {
  return userPromise().then(data => {
    expect(data).toBe('123');
  })
})

3.3 使用内置 API 检测 Promise

Jest 提供了相当一部分的便捷式 API 方便我们对 Promise 进行测试,例如:

const userPromise = () => Promise.resolve('hello');
it('test with expect', () => {
  return expect(userPromise()).resolves.toBe('hello');
})

类似上述的 API 还有:

const rejectPromise = () => Promise.reject('error');
it('test with expect reject', () => {
  return expect(rejectPromise()).rejects.toBe('hello');
})

3.4 使用 async 函数检测异步

const userPromise = () => Promise.resolve('hello');
it('test with async', async () => {
  const data = await userPromise();
  expect(data).toBe('hello');
})

4. Mock 函数

在 jest 中,提供了功能强大的内置 Mock ,最常见的就是使用 Mock 来模拟一个回调函数:

function mockTest(shouldCall, cb){
  if (shouldCall) { return cb(42) }
}

上面的这个函数的第二个参数接受的是一个回调函数,在测试的时候,我们可以 Mock 一个函数来充当这个回调函数;不仅如此,Mock 出来的函数甚至还具有统计功能:

it('test with mock function', () => {
  const mockCb = jest.fn();
  mockTest(true, mockCb);
      expect(mockCb).toHaveBeenCalled(); // 期待mockCb被调用了
      expect(mockCb).toHaveBeenCalledWith(42); // 检查调用回调函数的时候传入的参数为42
      expect(mockCb).toHaveBeenCalledTimes(1); // 检查回调函数被调用的次数
      console.log(mockCb.mock.calls);
      console.log(mockCb.mock.results);
})

从上面的代码中可以清除的看出来,Mock 出来充当回调函数的 mockCb 是一个带有统计功能的假函数。从中可以提取出调用它的环境的一些信息。

4.1 Mock 一个有实际含义的函数

上面的过程中 mockCb 虽然展示了其统计功能,但是却没有实际的意义,下面就 Mock 一个具有实际用途的函数来:

it('test mock with implementation', () => {
  const mockCb = jest.fn(x=>x*2);
  mockTest(true, mockCb);
  console.log(mockCb.mock.results);
})

不难发现,Mock 一个具有实际意义的函数其实只需要在 jest.fn() 的入参中传入想要实现的函数即可。

有的时候,我们想要更加快捷,比如说模拟一个返回值为 20 的函数,这个时候可以使用快捷的 API ,比如:

const mockCb = jest.fn().mockReturnValue(20);

4.2 模拟库函数

有了上面的基础,相信你大概对 Mock 一个简单函数的过程有所了解了吧;那么接下来,让我们更进一步,说一说如何去 Mock 库函数。下面就以最常使用的 axios 为例说明如何模仿库函数

const axios = require('axios');
jest.mock('axios');
axios.get.mockImplementation(() => {
  return Promise.resolve({data: {username: 'viking'}});
})

下面我逐步解释代码中的每个部分:

  1. const axios = require('axios');
  • 这行代码引入了 axios 库,它是一个流行的 HTTP 客户端库,用于在 Node.js 或浏览器端发送 HTTP 请求。
  1. jest.mock('axios');
  • 这行代码告诉 Jest 测试框架去模拟(mock)axios 模块。模拟意味着在测试环境中,当我们尝试引入 axios,我们不是得到一个实际的 axios 实例,而是一个模拟的版本。这样做的目的是在测试中控制 axios 的行为,而不是让它实际去发送 HTTP 请求。
  1. mockImplementation
axios.get.mockImplementation(() => {
  return Promise.resolve({data: {username: 'viking'}});
});
  • 这部分代码定义了当测试中的代码调用 axios.get 方法时,它应该如何表现。具体来说,我们为 axios.get 提供了一个新的实现(mockImplementation)。
  • () => { ... } 是一个箭头函数,它不接受任何参数(由空括号 () 表示)并返回一个 Promise
  • Promise.resolve({data: {username: 'viking'}}) 创建并立即解析一个包含特定数据({username: 'viking'})的 Promise
  • 这意味着,在测试中每当 axios.get 被调用时,它不会真正去执行一个 HTTP GET 请求,而是立即返回一个解析后的 Promise,该 Promise 包含一个具有 username 属性的数据对象。

这段代码的主要意图是在 Jest 测试环境中模拟 Axios 的 GET 请求行为。通过模拟,测试可以更加可控,因为我们知道每次调用 axios.get 时会返回什么,而不需要依赖外部服务器或网络连接。这在单元测试中特别有用,因为我们希望测试的是代码单元的逻辑,而不是外部依赖的行为。


下面来演示一下其用法: 为了完成getUserName函数,并使其能够被测试,我们首先需要定义getUserName函数,并确保在这个函数中我们使用了axios.get方法来模拟一个HTTP GET请求。此外,我们也需要模拟axios模块以便在测试中检查其是否被正确调用。

以下是一个可能的getUserName函数的实现,以及如何在测试文件中模拟axios

首先是getUserName函数的代码:

// 引入axios库
const axios = require('axios');

// 定义getUserName函数,假设该函数根据用户ID获取用户名
async function getUserName(userId) {
  try {
    const response = await axios.get(`https://api.example.com/users/${userId}`);
    return response.data.username; // 假设API返回的数据中包含username字段
  } catch (error) {
    console.error('Error fetching user name:', error);
    throw error; // 重新抛出错误以便调用者处理
  }
}

module.exports = getUserName;

接下来是测试文件的代码,包括axios的模拟:

const axios = require('axios');
const getUserName = require('./getUserName'); // 假设getUserName函数在getUserName.js文件中定义

// 使用jest.mock来模拟axios模块
jest.mock('axios');

test('test with mock modules', async () => {
  // 模拟axios.get的返回值
  axios.get.mockResolvedValue({ data: { username: 'JohnDoe' } });

  // 调用getUserName函数并等待其返回结果
  const name = await getUserName(1);
  console.log(name); // 输出: JohnDoe

  // 验证axios.get是否被调用过
  expect(axios.get).toHaveBeenCalled();
  // 验证axios.get被调用的次数
  expect(axios.get).toHaveBeenCalledTimes(1);
});

在这个例子中,我们使用jest.mock('axios')来模拟axios模块,并使用axios.get.mockResolvedValue来设置模拟的返回值。这样,在调用getUserName函数时,axios.get将返回一个模拟的响应,而不是实际发送HTTP请求。

4.3 全局 Mock 库函数

在上一小节中,我们只是在特定的测试文件中对 axios 进行了劫持。那么为了不出现在每一个测试文件中都重复的劫持同一个库函数的事情发生,我们可以使用全局劫持的功能。那么在 Jest 中如何实现全局劫持呢?

涉及到全局劫持,就必须从 Jest 的文件结构入手了。

假如我们需要全局劫持 axios 库函数,则需要:

  • 在项目的根目录下创建__mocks__/axios.js 文件,请注意这个文件的名字必须同名,也就是 axios.js
  • 然后在 axios.js 中写入如下的代码:
const axios = {
  get: jest.fn(()=>Promise.resolve({ data: { username: 'JohnDoe' } }))
}
module.exports = axios;

此文件只需要将构建的对象暴露出去即可,剩下的事情交给 Jest 框架来处理,它会自动将其推广至全局。

其实通过对比就不难发现,在全局劫持 axios 的方式和在单个文件中使用的方法是不一样的,在全局劫持本质上是自己制作了一个新的同名库函数。这个时候和 axios 本来的实现就没有关系了。

具体来说,在测试文件中,如果使用下面的方式引入 axios:

const axios = require('axios');

那么得到的 axios 对象就是 axios.js 已经注入到全局中的对象,而不是第三方库。

4.4 Mock 方法罗列

在 Jest 中,当你创建一个 mock 函数(jest.fn())或者使用 jest.spyOn() 时,返回的 mock 函数对象包含一系列的方法和属性,允许你检查和操控 mock 函数的行为。以下是一些常用的方法和属性:

  1. .mockReturnValue(value): 设置当 mock 函数被调用时应返回的值。

  2. .mockReturnValueOnce(value): 设置当 mock 函数下一次被调用时应返回的值。之后调用的返回值需要再次使用此方法设置。

  3. .mockImplementation(fn): 提供一个函数,该函数将作为 mock 函数的实现。你可以在这个函数内部定义 mock 函数的行为。

  4. .mockImplementationOnce(fn): 提供一个函数,该函数将在 mock 函数下一次被调用时作为实现。之后调用的实现需要再次使用此方法设置。

  5. .mockReturnThis(): 设置 mock 函数在被调用时返回 this(调用 mock 函数的上下文对象)。

  6. .mockResolvedValue(value): 设置当 mock 函数作为 Promise 被调用时应解析为的值。

  7. .mockRejectedValue(value): 设置当 mock 函数作为 Promise 被调用时应拒绝并返回的值。

  8. .mockResolvedValueOnce(value) and .mockRejectedValueOnce(value): 设置 mock 函数作为 Promise 下一次被调用时应解析或拒绝并返回的值。

  9. .mockName(name): 为 mock 函数设置一个自定义名称,这有助于在测试输出中识别它。

  10. .getMockName(): 获取 mock 函数的名称。

  11. .calls: 一个包含所有对 mock 函数的调用的数组。每个调用都是一个包含调用参数和实例的数组。

  12. .instances: mock 构造函数创建的实例数组(如果 mock 函数被用作构造函数)。

  13. .invocationCallOrder: 一个列表,显示 mock 函数被调用的顺序(在所有 mocks 中)。

  14. .results: 一个包含每次调用的结果的数组。

此外,还有一些用于断言和检查的方法:

  • .toHaveBeenCalled(): 检查 mock 函数是否被调用过。
  • .toHaveBeenCalledTimes(number): 检查 mock 函数被调用的次数。
  • .toHaveBeenCalledWith(...args): 检查 mock 函数是否使用特定参数被调用。
  • .toHaveBeenCalledWithExactly(...args): 检查 mock 函数是否使用特定参数且仅这些参数被调用。
  • .toHaveBeenLastCalledWith(...args): 检查 mock 函数最后一次调用时使用的参数。
  • .toHaveBeenNthCalledWith(nthCall, ...args): 检查 mock 函数在第 n 次调用时使用的参数。
  • .toHaveReturned(): 检查 mock 函数是否返回过值。
  • .toHaveReturnedTimes(number): 检查 mock 函数返回值的次数。
  • .toHaveReturnedWith(value): 检查 mock 函数是否返回过特定值。
  • .toReturnTimes(number) (已弃用): 检查 mock 函数返回的次数。
  • .toReturnWith(value) (已弃用): 检查 mock 函数是否返回特定值。

5. 定时器的使用

在 Jest 中如何处理 setTimeout 呢?实际上,Jest 可以控制时间的前进,因此处理定时任务来说相当得心应手:

function fetchUser(callback) { setTimeout(() => { callback('hello'); }, 1000); }

jest.useFakeTimers();
it('test the callback after 1 sec', () => {
  const cb = jest.fn();
  fetchUser(cb);
  expect(cb).not.toHaveBeenCalled();
  expect(setTimeout).toHaveBeenCalledTimes(1);
  jest.runAllTimers();
  expect(cb).not.toHaveBeenCalled();
  expect(cb).not.toHaveBeenCalledWith('hello');
})

expect(setTimeout).toHaveBeenCalledTimes(1); 不难看出,jest.useFakeTimers(); 执行之后 setTimeout 就被劫持了,不然怎么拥有的统计功能?

其次, jest.runAllTimers(); 可以运行完所有的定时器。

5.1 更加细粒度的控制定时器

那么可不可以更加细粒度的控制定时器呢?

Jest 提供了三大 API 完成不同粒度的时间控制,它们分别是:

  • jest.runAllTimers
  • jest.runOnlyPendingTimers
  • jest.advanceTimersByTime

在 Jest 中,当你使用 jest.useFakeTimers() 启用了模拟计时器后,可以使用一些特定的 API 来控制这些计时器的执行。下面是三种常用的时间控制 API 的说明和示例:

jest.runAllTimers()

jest.runAllTimers() 会立即执行所有挂起的计时器。这在你想要一次性触发所有定时器时非常有用。

jest.useFakeTimers();

test('runs all timers immediately', () => {
  const callback = jest.fn();

  setTimeout(callback, 1000); // 设置一个1秒后执行的定时器
  setTimeout(callback, 2000); // 设置一个2秒后执行的定时器

  jest.runAllTimers(); // 运行所有定时器

  expect(callback).toHaveBeenCalledTimes(2); // 回调函数被调用了两次
});

jest.runOnlyPendingTimers()

jest.runOnlyPendingTimers()jest.runAllTimers() 类似,但它只会运行那些已经到期的定时器,而不是所有挂起的定时器。这意味着,如果有多个定时器设置了不同的延迟,只有那些延迟时间已经过去的定时器才会被执行。

jest.useFakeTimers();

test('runs only pending timers', () => {
  const callback = jest.fn();

  setTimeout(callback, 1000); // 1秒后执行
  setTimeout(callback, 3000); // 3秒后执行

  jest.advanceTimersByTime(2000); // 时间前进2秒
  expect(callback).not.toHaveBeenCalled(); // 还没有定时器到期

  jest.runOnlyPendingTimers(); // 运行已经到期的定时器
  expect(callback).toHaveBeenCalledTimes(1); // 只有第一个定时器到期并执行了
});

jest.advanceTimersByTime(msToRun)

jest.advanceTimersByTime(msToRun) 允许你按指定的毫秒数前进模拟时钟的时间。这对于测试具有特定时间间隔的逻辑非常有用,因为你可以精确地控制时间的流逝。

jest.useFakeTimers();

test('advances timers by a specified time', () => {
  const callback = jest.fn();

  setTimeout(callback, 1500); // 1.5秒后执行
  setTimeout(callback, 3000); // 3秒后执行

  jest.advanceTimersByTime(1000); // 时间前进1秒
  expect(callback).not.toHaveBeenCalled(); // 回调函数尚未被调用

  jest.advanceTimersByTime(500); // 时间再前进0.5秒,累计前进1.5秒
  expect(callback).toHaveBeenCalledTimes(1); // 第一个定时器到期并执行了

  jest.advanceTimersByTime(1500); // 时间再前进1.5秒,累计前进3秒
  expect(callback).toHaveBeenCalledTimes(2); // 第二个定时器也到期并执行了
});

参考资料: Jest 框架入门教程