首页 分类
阿荡的博客

在jest单元测试中模拟接口请求

创建时间:2021-01-05

更新时间:20 小时前

本文先用三个步骤,简单实现一下 jest 模拟接口请求。然后循序渐进,给出另一个更好的可选的方案。在阅读本文之前,你应当知晓如何使用 jest。

先从一个例子开始,有个index.js文件,使用axios请求后端接口,代码如下:

// index.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;

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

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

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

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

  1. 在测试文件中导入需要模拟的模块
  2. 使用jest.mock()方法 mock 一下模块
  3. 使用.mockResolvedValue()模拟数据返回

index.test.js进行修改:

// index.test.js
const getFirstAlbumTitle = require("./index");
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'),在测试文件index.test.js中的所有axio请求都会被接管,包括在该文件中导入的getFirstAlbumTitle中的axios其他测试文件不受影响

至此,我们已经知道了如何模拟axios请求,这种方案暂且命名为方案 1。 方案 1 有个缺点,就是模拟的数据和测试用例杂糅在一起,如果模拟的数据过于庞大,远超过测试用例的代码量,这就有点混乱了,测试用例应该注重逻辑的书写,而不是人为模拟的数据。 接下来说的是方案 2,讲的是如何把模拟的数据和测试用例分开。 在项目根目录下新建__mocks__目录,大小写不能错,在__mocks__目录下,新建一个和axios齐名的js文件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",
          },
        ],
      });
    });
  },
};

index.test.js修改如下

// index.test.js
const getFirstAlbumTitle = require("./index");
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.get.mockResolvedValue替换成另外一种种实现形式,就是__mocks__目录下的文件axios.js。注意,方案 2 不适合模拟项目里自定义的接口请求模块。 为了模拟我们自己手写的封装好的自定义模块,只需要在和自定义模块的同一目录下,新建一个目录__mocks__,然后在__mocks__目录下新建一个和自定义模块齐名的js文件就可以了,本质上和方案 2 相同,只是目录__mocks__存放的位置不同。

举例,存在一个封装好接口请求的自定义模块,目录为moduls/request.js,要对它的请求进行模拟,需要新建一个文件moduls/__mocks__/request.js。 注意在调用jest.mock模拟request.js的时候,应当这样写:

jest.mock("./moduls/request.js"); // 将文件目录作为参数传递给 jest.mock

而不是下面这种写法:

const request = require("./moduls/request.js");
jest.mock("request"); // 这样会让jest误以为模拟的是第三方模块 request

全文完。