jest 是前端常用的测试框架,可以用来测试我们手写的方法,比如下面的 sum 方法,

// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require("./sum");

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});

在运行 npm run test 时候,测试用例跑得非常快。但是如果方法中涉及到接口请求,接口响应很慢,测试用例跑起来会很慢。

比如下面这个例子,有个 getFirstAlbumTitle.js 文件,使用 axios 请求后端接口,代码如下:

// getFirstAlbumTitle.js
const axios = require("axios");

async function getFirstAlbumTitle() {
  const response = await axios.get(
    "https://jsonplaceholder.typicode.com/albums"
  );
  return response.data[0].title;
}

module.exports = getFirstAlbumTitle;

然后,我们要对它进行测试, getFirstAlbumTitle.test.js 代码如下:

// getFirstAlbumTitle.test.js
const getFirstAlbumTitle = require("./getFirstAlbumTitle.js");

it("returns the title of the first album", async () => {
  const title = await getFirstAlbumTitle();
  expect(title).toEqual("quidem molestiae enim");
});

如果不模拟接口请求,每次跑测试用例,都会真实地请求 albums 接口,严重拖慢测试的速度。并且接口的响应值常常是不固定的,title 并不一定会返回 quidem molestiae enim,导致测试失败。

我们关心的其实不是接口是否返回了数据,应该是处理数据的逻辑是否正确 —— response.data[0].title ,所以我们可以模拟 axios 的返回值。

按照以下步骤,改造一下 getFirstAlbumTitle.test.js

  1. 使用 jest.mock() 方法 mock 一下 axios 模块
  2. 使用 mockResolvedValue 方法模拟数据返回

完整的代码如下:

// getFirstAlbumTitle.test.js
const getFirstAlbumTitle = require("./getFirstAlbumTitle.js");
const axios = require("axios");

jest.mock("axios");

it("returns the title of the first album", async () => {
  axios.get.mockResolvedValue({
    data: [
      {
        userId: 1,
        id: 1,
        title: "My First Album",
      },
      {
        userId: 1,
        id: 2,
        title: "Album: The Sequel",
      },
    ],
  });

  const title = await getFirstAlbumTitle();
  expect(title).toEqual("My First Album");
});

这里面最核心的语句是 jest.mock('axios') ,意思是 jest 接管了 axios 的所有行为(方法),如果不使用 mockResolvedValue 方法,本文件中axios 的所有方法都将返回 undefined

const axios = require("axios");
jest.mock("axios");

// Does nothing, then returns undefined:
axios.get("https://www.google.com");

// Does nothing, then returns undefined:
axios.post("https://jsonplaceholder.typicode.com/albums", {
  id: 3,
  title: "Album with a Vengeance",
});

注意

jest.mock() 应该在外层调用,而不是在 it 中调用。

一旦调用了 jest.mock('axios') ,在测试文件 getFirstAlbumTitle.test.js 中的所有 axio 请求都会被接管,包括在该文件中导入的 getFirstAlbumTitle 中的 axios其他测试文件不受影响

这个方案有个缺点,就是模拟的数据和测试用例杂糅在一起,如果模拟的数据过于庞大,远超过测试用例的代码量,这就有点混乱了,测试用例应该注重逻辑的书写,而不是人为模拟的数据。

接下来改进这个方案,把模拟的数据和测试用例分开。

在项目根目录下新建一个 __mocks__ 目录,大小写不能错,在 __mocks__ 目录下,新建一个和 axios 相同名字的文件 axios.js ,加入如下代码:

// axios.js
module.exports = {
  get: function () {
    return new Promise(function (resolve) {
      resolve({
        data: [
          {
            userId: 1,
            id: 1,
            title: "My First Album",
          },
          {
            userId: 1,
            id: 2,
            title: "Album: The Sequel",
          },
        ],
      });
    });
  },
};

getFirstAlbumTitle.test.js 修改如下

// getFirstAlbumTitle.test.js
const getFirstAlbumTitle = require("./getFirstAlbumTitle.js");
const axios = require("axios");

jest.mock("axios");

it("returns the title of the first album", async () => {
  const title = await getFirstAlbumTitle();
  expect(title).toEqual("My First Album");
});

如果想根据不同的接口返回不同的数据,需要改造下 axios.js 文件

module.exports = {
  get: function (apiPath) {
    // 根据apiPath返回不同的数据
    // ...
  },
};

注意,改进后的方案不适合模拟项目里自定义的接口请求模块,比如你写了一个模块 request.js,里面封装了 fetch。

为了模拟我们自己手写的封装好的自定义模块,只需要在和自定义模块的同一目录下,新建一个目录 __mocks__ ,然后在 __mocks__ 目录下新建一个和自定义模块相同名字的 js 文件就可以了。

举个例子,存在一个封装 fetch 的自定义模块,目录为 modules/request.js ,代码如下

// modules/request.js
function request(url, options = {}) {
  return fetch(url, options)
    .then((response) => {
      if (!response.ok) {
        throw new Error("Network response was not ok");
      }
      return response.json();
    })
    .catch((error) => {
      console.error("Fetch error:", error);
      throw error; // 重新抛出错误,以便调用者可以进一步处理
    });
}
module.exports = request;

getFirstAlbumTitle 方法使用 request 模块调用接口

// getFirstAlbumTitle.js
const request = require("./modules/request.js");

async function getFirstAlbumTitle() {
  const response = await request("https://jsonplaceholder.typicode.com/albums");
  return response.data[0].title;
}

module.exports = getFirstAlbumTitle;

想要在测试文件 getFirstAlbumTitle.test.js 中模拟 request 模块的返回值,需要新建一个文件 modules/__mocks__/request.js ,注意此时的 __mocks__ 位置和 request.js 同一级。

在调用 jest.mock 模拟 request.js 的时候,应当把 request.js 的路径传进去:

// getFirstAlbumTitle.test.js
const getFirstAlbumTitle = require("./getFirstAlbumTitle.js");

jest.mock("./modules/request.js");

it("returns the title of the first album", async () => {
  const title = await getFirstAlbumTitle();
  expect(title).toEqual("My First Album");
});

下面写法是错的

const request = require("./modules/request.js");
jest.mock("request");