Jest中Mock网络请求

最近需要将一个比较老的库修改为TS并进行单元测试,修改为TS还能会一点,单元测试纯粹是现学现卖了,初学Jest框架,觉得在单元测试中比较麻烦的就是测试网络请求,所以记录一下MockAxios发起网络请求的一些方式。初学两天的小白,如有问题还请指出。

描述

文中提到的示例全部在 jest-axios-mock-server仓库 中,直接使用包管理器安装就可以启动示例,例如通过yarn安装:

$ yarn install

package.json中指定了一些命令,分别如下:

  • npm run build: rollup的打包命令。
  • npm run test:demo1: 简单地mock封装的网络请求库。
  • npm run test:demo2: 采用重新实现并hook的方式完成mock
  • npm run test:demo3: 使用Jest中的库完成demo2的实现。
  • npm run test:demo4-5: 启动一个node服务器,通过axiosproxy将网络请求进行代理,转发到启动的node服务器,通过设置好对应的单元测试请求与响应的数据,利用对应关系实现测试,也就是jest-axios-mock-server完成的工作。

在这里我们封装了一层axios,比较接近真实场景,可以查看test/demo/wrap-request.ts文件,实际上只是简单的在内部创建了一个axios实例,并且转发了一下响应的数据而已,test/demo/index.ts文件简单地导出了一个counter方法,这里对于这两个参数有一定的处理然后才发起网络请求,之后对于响应的数据也有一定的处理,只是为了模拟一下相关的操作而已。

// test/demo/wrap-request.ts
import axios, { AxiosRequestConfig } from "axios"; const instance = axios.create({
timeout: 3000,
}); export const request = (options: AxiosRequestConfig): Promise<any> => {
// do something wrap
return instance.request(options).then(res => res.data);
};
// test/demo/index.ts
import { request } from "./wrap-request"; export const counter = (id: number, number: number): Promise<{ result: number; msg: string }> => {
const operate = number > 0 ? 1 : -1;
return request({
url: "https://www.example.com/api/setCounter",
method: "POST",
data: { id, operate },
})
.then(res => {
if (res.result === 0) return { result: 0, msg: "success" };
if (res.result === -100) return { result: -100, msg: "need login" };
return { result: -999, msg: "fail" };
})
.catch(err => {
return { result: -999, msg: "fail" };
});
};

此处的Jest使用了JSDOM模拟的浏览器环境,在jest.config.js中配置的setupFiles属性中配置了启动文件test/config/setup.js,在此处初始化了JSDOM

import { JSDOM } from "jsdom";

const config = {
url: "https://www.example.com/",
domain: "example.com",
};
const dom = new JSDOM("", config);
global.document = dom.window.document;
global.document.domain = config.domain;
global.window = dom.window;
global.location = dom.window.location;

demo1: 简单Mock网络请求

test/demo1.test.js中进行了简单的mock处理,通过npm run test:demo1即可尝试运行,实际上是将包装axioswrap-request库进行了一个mock操作,在Jest启动时会进行编译,在这里将这个库mock掉后,所有在之后引入这个库的文件都是会获得mock后的对象,也就是说我们可以认为这个库已经重写了,重写之后的方法都是JESTMock Functions了,可以使用诸如mockReturnValue一类的函数进行数据模拟,关于Mock Functions可以参考https://www.jestjs.cn/docs/mock-functions

// test/demo1.test.js
import { counter } from "./demo";
import { request } from "./demo/wrap-request"; jest.mock("./demo/wrap-request"); describe("Simple mock", () => {
it("test success", () => {
request.mockResolvedValue({ result: 0 });
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: 0, msg: "success" });
});
}); it("test need login", () => {
request.mockResolvedValue({ result: -100 });
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: -100, msg: "need login" });
});
}); it("test something wrong", () => {
request.mockResolvedValue({ result: 1111111 });
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: -999, msg: "fail" });
});
});
});

在这里我们完成了返回值的Mock,也就是说对于wrap-request库中的request返回的值我们都能进行控制了,但是之前也提到过对于传入的参数也有一定的处理,这部分内容我们还没有进行断言,所以对于这个我们同样需要尝试进行处理。

demo2: hook网络请求

demo2通过npm run test:demo2即可尝试运行,在上边提到了我们可以处理返回值的情况,但是没法断言输入的参数是否正确进行了处理,所以我们需要处理一下这种情况,所幸Jest提供了一种可以直接实现被Mock的函数库的方式,当然实际上Jest还提供了mockImplementation的方式,这个是在demo3中使用的方式,在这里我们重写了被mock的函数库,在实现的时候也可以使用jest.fn完成Implementations,这里通过在返回之前写入了一个hook函数,并且在各个test时再实现断言或者是指定返回值,这样就可以解决上述问题,实际上就是实现了JestMock FunctionsmockImplementation

// test/demo2.test.js
import { counter } from "./demo";
import * as request from "./demo/wrap-request"; jest.mock("./demo/wrap-request", () => {
let hook = () => ({ result: 0 });
return {
setHook: cb => (hook = cb),
request: (...args) => {
return new Promise(resolve => {
resolve(hook(...args));
});
},
};
}); describe("Simple mock", () => {
it("test success", () => {
request.setHook(() => ({ result: 0 }));
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: 0, msg: "success" });
});
}); it("test need login", () => {
request.setHook(() => ({ result: -100 }));
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: -100, msg: "need login" });
});
}); it("test something wrong", () => {
request.setHook(() => ({ result: 1111111 }));
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: -999, msg: "fail" });
});
}); it("test param transform", () => {
return new Promise(done => {
request.setHook(({ data }) => {
expect(data).toStrictEqual({ id: 1, operate: 1 });
done();
return { result: 0 };
});
counter(1, 1000);
});
});
});

demo3: 使用Jest的mockImplementation

demo3通过npm run test:demo3即可尝试运行,在demo2中的例子实际上是写复杂了,在JestMock FunctionsmockImplementation的实现,直接使用即可。

// test/demo3.test.js
import { counter } from "./demo";
import { request } from "./demo/wrap-request"; jest.mock("./demo/wrap-request"); describe("Simple mock", () => {
it("test success", () => {
request.mockImplementation(() => Promise.resolve({ result: 0 }));
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: 0, msg: "success" });
});
}); it("test need login", () => {
request.mockImplementation(() => Promise.resolve({ result: -100 }));
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: -100, msg: "need login" });
});
}); it("test something wrong", () => {
request.mockImplementation(() => Promise.resolve({ result: 1111111 }));
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: -999, msg: "fail" });
});
}); it("test param transform", () => {
return new Promise(done => {
request.mockImplementation(({ data }) => {
expect(data).toStrictEqual({ id: 1, operate: 1 });
done();
return Promise.resolve({ result: 0 });
});
counter(1, 1000);
});
});
});

demo4-5: 真实发起网络请求

demo4demo5通过npm run test:demo4-5即可尝试运行,采用这种方式是进行了真正的数据请求,在这里会利用axios的代理,将内部的数据请求转发到指定的服务器端口,当然这个服务器也是在本地启动的,通过指定对应的path相关的请求与响应数据进行测试,如果请求的数据不正确,则不会正常匹配到相关的响应数据,这样这个请求会直接返回500,返回的响应数据如果不正确的话也会在断言时被捕捉。在这里就使用到了jest-axios-mock-server库,首先我们需要指定三个文件,分别对应每个单元测试文件启动前执行,Jest测试启动前执行,与Jest测试完成后执行的三个生命周期进行的操作,分别是jest.config.js配置文件的setupFilesglobalSetupglobalTeardown三个配置项。

首先是setupFiles,在这里我们除了初始化JSDOM之外,还需要对axios的默认代理进行操作,因为采用的方案是使用axiosproxy进行数据请求的转发,所以才需要在单元测试的最前方设定代理值。

// test/config/setup.js
import { JSDOM } from "jsdom";
import { init } from "../../src/index";
import axios from "axios"; const config = {
url: "https://www.example.com/",
domain: "example.com",
};
const dom = new JSDOM("", config);
global.document = dom.window.document;
global.document.domain = config.domain;
global.window = dom.window;
global.location = dom.window.location; init(axios);

之后便是globalSetupglobalTeardown两个配置项,在这里指的是Jest单元测试启动前与全部测试完毕后进行的操作,我们将服务器启动与关闭的操作都放在这里,请注意,在这两个文件运行的文件是单独的一个独立context,与任何进行的单元测试的context都是无关的,包括setupFiles配置项指定的文件,所以在此处所有的数据要么是通过在配置文件中指定,要不就是通过网络在服务器端口之间进行传输。

// test/config/global-setup.js
import { run } from "../../src";
export default async () => {
await run();
};
// test/config/global-teardown.js
import { close } from "../../src";
export default async function () {
await close();
}

对于配置端口与域名信息,将其直接放置在jest.config.js中的globals字段中了,对于debug这个配置项,建议和test.only配合使用,在调用服务器信息的过程中可以打印出相关的请求信息。

// jest.config.js
module.exports = {
// ...
globals: {
host: "127.0.0.1",
port: "5000",
debug: false,
},
// ...
}

当然,或许会有提出为什么不在每个单元测试文件的beforeAllafterAll生命周期启动与关闭服务器,首先这个方案我也尝试过,首先对于每个测试文件将服务器启动结束后再关闭虽然相对比较耗费时间,但是理论上还是合理的,毕竟要进行数据隔离的话确实是没错,但是在afterAll关闭的时候就出了问题,因为node服务器在关闭时调用的close方法并不会真实地关闭服务器以及端口占用,他只是停止处理请求了,端口还是被占用,当启动第二个单元测试文件时会抛出端口正在被占用的异常,虽然现在已经有一些解决的方案,但是我尝试过后并不理想,会偶现端口依旧被占用的情况,尤其是在node开机后第一次被运行的情况,异常的概率比较大,所以效果不是很理想,最终还是采用了这种完全隔离的方案,具体相关的问题可以参考https://stackoverflow.com/questions/14626636/how-do-i-shutdown-a-node-js-https-server-immediately

由于采用的是完全隔离的方案,所以我们想给测试的请求进行请求与响应数据的传输的时候,只有两个方案,要么在服务器启动的时候,也就是test/config/global-setup.js文件中将数据全部指定完成,要么就是通过网络进行数据传输,即在服务器运行的过程中通过指定path然后该path的网络请求会携带数据,在服务器的闭包中会把这个数据请求指定,当然在这里两种方式都支持,我觉得还是在每个单元测试文件中指定一个自己的数据比较合适,所以在这里仅示例了在单元测试文件中指定要测试的数据。关于要测试的数据,指定了一个DataMapper类型,以减少类型出错导致的异常,在这里示例了两个数据集,另外在匹配querydata时是支持正则表达式的,对于DataMapper类型的结构还是比较标准的。

// test/data/demo1.data.ts
import { DataMapper } from "../../src"; const data: DataMapper = {
"/api/setCounter": [
{
request: {
method: "POST",
data: '{"id":1,"operate":1}',
},
response: {
status: 200,
json: {
result: 0,
},
},
},
{
request: {
method: "POST",
data: /"id":2,"operate":-1/,
},
response: {
status: 200,
json: {
result: -100,
},
},
},
],
}; export default data;
// test/data/demo2.data.ts
import { DataMapper } from "../../src"; const data: DataMapper = {
"/api/setCounter": [
{
request: {
method: "POST",
data: /"id":3,"operate":-1/,
},
response: {
status: 200,
json: {
result: -100,
},
},
},
],
}; export default data;

最后进行的两个单元测试中就在beforeAll中指定了要测试的数据,要注意这里是return setSuitesData(data),因为要在数据设置成功响应以后在进行单元测试,之后就是正常的请求与响应以及断言测试是否正确了。

// test/demo4.test.js
import { counter } from "./demo";
import { setSuitesData } from "../src/index";
import data from "./data/demo1.data"; beforeAll(() => {
return setSuitesData(data);
}); describe("Simple mock", () => {
it("test success", () => {
return counter(1, 2).then(res => {
expect(res).toStrictEqual({ result: 0, msg: "success" });
});
}); it("test need login", () => {
return counter(2, -3).then(res => {
expect(res).toStrictEqual({ result: -100, msg: "need login" });
});
});
});
// test/demo5.test.js
import { counter } from "./demo";
import { setSuitesData } from "../src/index";
import data from "./data/demo2.data"; beforeAll(() => {
return setSuitesData(data);
}); describe("Simple mock", () => {
it("test success", () => {
return counter(3, -30).then(res => {
expect(res).toStrictEqual({ result: -100, msg: "need login" });
});
}); it("test no match response", () => {
return counter(6, 2).then(res => {
expect(res).toStrictEqual({ result: -999, msg: "fail" });
});
});
});

BLOG

https://github.com/WindrunnerMax/EveryDay/

参考

https://www.jestjs.cn/docs/mock-functions
https://stackoverflow.com/questions/41316071/jest-clean-up-after-all-tests-have-run
https://stackoverflow.com/questions/14626636/how-do-i-shutdown-a-node-js-https-server-immediately

Jest中Mock网络请求的更多相关文章

  1. swift中第三方网络请求库Alamofire的安装与使用

    swift中第三方网络请求库Alamofire的安装与使用 Alamofire是swift中一个比较流行的网络请求库:https://github.com/Alamofire/Alamofire.下面 ...

  2. React Native中的网络请求fetch和简单封装

    React Native中的网络请求fetch使用方法最为简单,但却可以实现大多数的网络请求,需要了解更多的可以访问: https://segmentfault.com/a/1190000003810 ...

  3. Cocos2d-X多线程(4) 在子线程中进行网络请求

    新版本的android系统已经不允许在UI线程中进行网络请求了,必须新建一个线程. 代码实操: 头文件: #ifndef __TestThreadHttp_SCENE_H__ #define __Te ...

  4. iOS中的网络请求 和 网络监测

    1.网络监测 //根据主机名判断网络是否连接 Reachability *reach = [Reachability reachabilityWithHostName:@"www.baidu ...

  5. Android中解析网络请求的URL

    近期正在做Android网络应用的开发,使用了android网络请求方面的知识.如今向大家介绍网络请求方面的知识.我们知道android中向server端发送一个请求,(这就是我们通常所说的POST请 ...

  6. iOS项目中的网络请求和上下拉刷新封装

    代码地址如下:http://www.demodashi.com/demo/11621.html 一.运行效果图 现在的项目中不可避免的要使用到网络请求,而且几乎所有软件都有上下拉刷新功能,所以我在此对 ...

  7. ios中封装网络请求类

    #import "JSNetWork.h" //asiHttpRequest #import "ASIFormDataRequest.h" //xml 的解析 ...

  8. 在iOS9中 xcode7 网络请求 如图片请求不显示等

    Application Transport Security has blocked a cleartext HTTP (http://) resource load since it is inse ...

  9. flutter中的网络请求和下拉刷新上拉加载,toast的案例

    添加依赖 pull_to_refresh: ^1.5.6 dio: ^2.1.0 fluttertoast: ^3.0.1 DioUtil import 'package:dio/dio.dart'; ...

随机推荐

  1. 论文笔记:(NIPS2018)PointCNN: Convolution On X-Transformed Points

    目录 摘要 一.2D卷积应用在点云上存在的问题 二.解决的方法 2.1 idea 2.2 X-conv算子 2.3 分层卷积 三.实验 3.1分类和分割 3.2消融实验.可视化和模型复杂度 总结 仍存 ...

  2. 音视频开发之H.264 入门知识

    大家如果有做过音视频相关的项目,那么肯定对 H.264 相关的概念了解的比较通透,这里我为什么还要写这样一篇文章呢?一来是为了对知识的总结,二来是为了给刚入门音视频的同学一个参考. 基础概念 H.26 ...

  3. 干了5年Android开发,突然感觉自己啥也不会,啥也不想干,还要继续吗?

    这是在某论坛看到的一名同行的吐槽: 我干了差不多5年,不过给人感觉跟只有两三年的人一样. 我觉得我不适合干程序员,主要是新东西的接受能力比其他人慢,Android技术又更新得很快,感觉总是跟不上.年纪 ...

  4. 走心的中级Android工程师跳槽经验分享

    这些经验是我最近四个月,从准备面试到找到合适工作的汗水和泪水,希望对你们能有帮助! define 跳槽 跳槽前要思考的问题 钱不到位怎么办 心委屈怎么办 离职前的思考 确定要走时需要做的准备 行情怎么 ...

  5. 我是如何在一晚上拿到阿里巴巴Android研发offer的?

    图文无关 开篇 我找工作时是2018年. 那一年,BAT大量缩招,就业形势严峻,互联网寒冬消息蔓延. 最终我经过激烈角逐拼下了几个大厂offer,回顾往事,觉得分享出来,也许对你能有所借鉴. 简历 这 ...

  6. Java线程基础及多线程的实现

    一.进程和线程 1.进程:正在运行的程序         是系统进行资源分配和调用的独立单位         每一个进程都有它自己的内存空间和系统资源 2.线程是进程中的单个顺序控制流,是一条执行路径 ...

  7. Vue2.x响应式原理

    一.回顾Vue响应式用法 ​ vue响应式,我们都很熟悉了.当我们修改vue中data对象中的属性时,页面中引用该属性的地方就会发生相应的改变.避免了我们再去操作dom,进行数据绑定. 二.Vue响应 ...

  8. 十进制转十六进制 BASIC-10

    十进制转十六进制 import java.util.Scanner; public class 十进制转十六进制 { /* 十六进制数是在程序设计时经常要使用到的一种整数的表示方式. * 它有0,1, ...

  9. 高效编程:在IntelliJ IDEA中使用VIM

    硬核干货分享,欢迎关注[Java补习课]成长的路上,我们一起前行 ! <高可用系列文章> 已收录在专栏,欢迎关注! 概述 Vim是一个功能强大.高度可定制的文本编辑器; 具体有多强大,我现 ...

  10. (五)Linux之文件与目录管理以及文本处理

    Linux之文件与目录管理 目录 Linux之文件与目录管理 前言 绝对路径与相对路径说明: 一.目录常用命令 常用处理目录的命令: 切换目录 cd 显示当前路径 pwd 查看目录下文件 ls 创建目 ...