全面掌握 Jest:从零开始的测试指南(下篇)
在上一篇测试指南中,我们介绍了Jest 的背景、如何初始化项目、常用的匹配器语法以及钩子函数的使用。这一篇篇将继续深入探讨 Jest 的高级特性,包括 Mock 函数、异步请求的处理、Mock 请求的模拟、类的模拟以及定时器的模拟、snapshot 的使用。通过这些技术,我们将能够更高效地编写和维护测试用例,尤其是在处理复杂异步逻辑和外部依赖时。
Mock 函数
假设存在一个 runCallBack 函数,其作用是判断入参是否为函数,如果是,则执行传入的函数。
export const runCallBack = (callback) => {
typeof callback == "function" && callback();
};
编写测试用例
我们先尝试编写它的测试用例:
import { runCallBack } from './func';
test("测试 runCallBack", () => {
const fn = () => {
return "hello";
};
expect(runCallBack(fn)).toBe("hello");
});
此时,命令行会报错提示 runCallBack(fn) 执行的返回值为 undefined,而不是 "hello"。如果期望得到正确的返回值,就需要修改原始的 runCallBack 函数,但这种做法不符合我们的测试预期——我们不希望为了测试而改变原有的业务功能。
这时,mock 函数就可以很好地解决这个问题。mock 可以用来模拟一个函数,并可以自定义函数的返回值。我们可以通过 mock 函数来分析其调用次数、入参和出参等信息。
使用 mock 解决问题
上述测试用例可以改为如下形式:
test("测试 runCallBack", () => {
const fn = jest.fn();
runCallBack(fn);
expect(fn).toBeCalled();
expect(fn.mock.calls.length).toBe(1);
});
这里,toBeCalled() 用于检查函数是否被调用过,fn.mock.calls.length 用于检查函数被调用的次数。
mock 属性中还有一些有用的参数:
- calls: 数组,保存着每次调用时的入参。
- instances: 数组,保存着每次调用时的实例对象。
- invocationCallOrder: 数组,保存着每次调用的顺序。
- results: 数组,保存着每次调用的执行结果。
自定义返回值
mock 还可以自定义返回值。可以在 jest.fn 中定义回调函数,或者通过 mockReturnValue、mockReturnValueOnce 方法定义返回值。
test("测试 runCallBack 返回值", () => {
const fn = jest.fn(() => {
return "hello";
});
createObject(fn);
expect(fn.mock.results[0].value).toBe("hello");
fn.mockReturnValue('alice') // 定义返回值
createObject(fn);
expect(fn.mock.results[1].value).toBe("alice");
fn.mockReturnValueOnce('x') // 定义只返回一次的返回值
createObject(fn);
expect(fn.mock.results[2].value).toBe("x");
createObject(fn);
expect(fn.mock.results[3].value).toBe("alice");
});
构造函数的模拟
构造函数作为一种特殊的函数,也可以通过 mock 实现模拟。
// func.js
export const createObject = (constructFn) => {
typeof constructFn == "function" && new constructFn();
};
// func.test.js
import { createObject } from './func';
test("测试 createObject", () => {
const fn = jest.fn();
createObject(fn);
expect(fn).toBeCalled();
expect(fn.mock.calls.length).toBe(1);
});
通过使用 mock 函数,我们可以更好地模拟函数的行为,并分析其调用情况。这样不仅可以避免修改原有业务逻辑,还能确保测试的准确性和可靠性。
异步代码
在处理异步请求时,我们期望 Jest 能够等待异步请求结束后再对结果进行校验。测试请求接口地址使用 http://httpbin.org/get,可以将参数通过 query 的形式拼接在 URL 上,如 http://httpbin.org/get?name=alice。这样接口返回的数据中将携带 { name: 'alice' },可以依此来对代码进行校验。

以下分别通过异步请求回调函数、Promise 链式调用、await 的方式获取响应结果来进行分析。
回调函数类型
回调函数的形式通过 done() 函数告诉 Jest 异步测试已经完成。
在 func.js 文件中通过 Axios 发送 GET 请求:
const axios = require("axios");
export const getDataCallback = (url, callbackFn) => {
axios.get(url).then(
(res) => {
callbackFn && callbackFn(res.data);
},
(error) => {
callbackFn && callbackFn(error);
}
);
};
在 func.test.js 文件中引入发送请求的方法:
import { getDataCallback } from "./func";
test("回调函数类型-成功", (done) => {
getDataCallback("http://httpbin.org/get?name=alice", (data) => {
expect(data.args).toEqual({ name: "alice" });
done();
});
});
test("回调函数类型-失败", (done) => {
getDataCallback("http://httpbin.org/xxxx", (data) => {
expect(data.message).toContain("404");
done();
});
});
promise类型
在 Promise 类型的用例中,需要使用 return 关键字来告诉 Jest 测试用例的结束时间。
// func.js
export const getDataPromise = (url) => {
return axios.get(url);
};
Promise 类型的函数可以通过 then 函数来处理:
// func.test.js
test("Promise 类型-成功", () => {
return getDataPromise("http://httpbin.org/get?name=alice").then((res) => {
expect(res.data.args).toEqual({ name: "alice" });
});
});
test("Promise 类型-失败", () => {
return getDataPromise("http://httpbin.org/xxxx").catch((res) => {
expect(res.response.status).toBe(404);
});
});
也可以直接通过 resolves 和 rejects 获取响应的所有参数并进行匹配:
test("Promise 类型-成功匹配对象t", () => {
return expect(
getDataPromise("http://httpbin.org/get?name=alice")
).resolves.toMatchObject({
status: 200,
});
});
test("Promise 类型-失败抛出异常", () => {
return expect(getDataPromise("http://httpbin.org/xxxx")).rejects.toThrow();
});
await 类型
上述 getDataPromise 也可以通过 await 的形式来编写测试用例:
test("await 类型-成功", async () => {
const res = await getDataPromise("http://httpbin.org/get?name=alice");
expect(res.data.args).toEqual({ name: "alice" });
});
test("await 类型-失败", async () => {
try {
await getDataPromise("http://httpbin.org/xxxx")
} catch(e){
expect(e.status).toBe(404)
}
});
通过上述几种方式,可以有效地编写异步函数的测试用例。回调函数、Promise 链式调用以及 await 的方式各有优劣,可以根据具体情况选择合适的方法。
Mock 请求/类/Timers
在前面处理异步代码时,是根据真实的接口内容来进行校验的。然而,这种方式并不总是最佳选择。一方面,每个校验都需要发送网络请求获取真实数据,这会导致测试用例执行时间较长;另一方面,接口格式是否满足要求是后端开发者需要着重测试的内容,前端测试用例并不需要涵盖这部分内容。
在之前的函数测试中,我们使用了 Mock 来模拟函数。实际上,Mock 不仅可以用来模拟函数,还可以模拟网络请求和文件。
Mock 网络请求
Mock 网络请求有两种方式:一种是直接模拟发送请求的工具(如 Axios),另一种是模拟引入的文件。
直接模拟 Axios
首先,在 request.js 中定义发送网络请求的逻辑:
import axios from "axios";
export const fetchData = () => {
return axios.get("/").then((res) => res.data);
};
然后,使用 jest 模拟 axios 即 jest.mock("axios"),并通过 axios.get.mockResolvedValue 来定义响应成功的返回值:
const axios = require("axios");
import { fetchData } from "./request";
jest.mock("axios");
test("测试 fetchData", () => {
axios.get.mockResolvedValue({
data: "hello",
});
return fetchData().then((data) => {
expect(data).toEqual("hello");
});
});
模拟引入的文件
如果希望模拟 request.js 文件,可以在当前目录下创建 __mocks__ 文件夹,并在其中创建同名的 request.js 文件来定义模拟请求的内容:
// __mocks__/request.js
export const fetchData = () => {
return new Promise((resolve, reject) => {
resolve("world");
});
};
使用 jest.mock('./request') 语法,Jest 在执行测试用例时会自动将真实的请求文件内容替换成 __mocks__/request.js 的文件内容:
// request.test.js
import { fetchData } from "./request";
jest.mock("./request");
test("测试 fetchData", () => {
return fetchData().then((data) => {
expect(data).toEqual("world");
});
});
如果部分内容需要从真实的文件中获取,可以通过 jest.requireActual() 函数来实现。取消模拟则可以使用 jest.unmock()。
Mock 类
假设在业务场景中定义了一个工具类,类中有多个方法,我们需要对类中的方法进行测试。
// util.js
export default class Util {
add(a, b) {
return a + b;
}
create() {}
}
// util.test.js
import Util from "./util";
test("测试add方法", () => {
const util = new Util();
expect(util.add(2, 5)).toEqual(7);
});
此时,另一个文件如 useUtil.js 也用到了 Util 类:
// useUtil.js
import Util from "./util";
export function useUtil() {
const util = new Util();
util.add(2, 6);
util.create();
}
在编写 useUtil 的测试用例时,我们只希望测试当前文件,并不希望重新测试 Util 类的功能。这时也可以通过 Mock 来实现。
在 __mock__ 文件夹下创建模拟文件
可以在 __mock__ 文件夹下创建 util.js 文件,文件中定义模拟函数:
// __mock__/util.js
const Util = jest.fn()
Util.prototype.add = jest.fn()
Util.prototype.create = jest.fn();
export default Util;
// useUtil.test.js
jest.mock("./util");
import Util from "./util";
import { useUtilFunc } from "./useUtil";
test("useUtil", () => {
useUtilFunc();
expect(Util).toHaveBeenCalled();
expect(Util.mock.instances[0].add).toHaveBeenCalled();
expect(Util.mock.instances[0].create).toHaveBeenCalled();
});
在当前 .test.js 文件定义模拟函数
也可以在当前 .test.js 文件中定义模拟函数:
// useUtil.test.js
import { useUtilFunc } from "./useUtil";
import Util from "./util";
jest.mock("./util", () => {
const Util = jest.fn();
Util.prototype.add = jest.fn();
Util.prototype.create = jest.fn();
return Util
});
test("useUtil", () => {
useUtilFunc();
expect(Util).toHaveBeenCalled();
expect(Util.mock.instances[0].add).toHaveBeenCalled();
expect(Util.mock.instances[0].create).toHaveBeenCalled();
});
这两种方式都可以模拟类。
Timers
在定义一些功能函数时,比如防抖和节流,经常会使用 setTimeout 来推迟函数的执行。这类功能也可以通过 Mock 来模拟测试。
// timer.js
export const timer = (callback) => {
setTimeout(() => {
callback();
}, 3000);
};
使用 done 异步执行
一种方式是使用 done 来异步执行:
import { timer } from './timer'
test("timer", (done) => {
timer(() => {
done();
expect(1).toBe(1);
});
});
使用 Jest 的 timers 方法
另一种方式是使用 Jest 提供的 timers 方法,通过 useFakeTimers 启用假定时器模式,runAllTimers 来手动运行所有的定时器,并使用 toHaveBeenCalledTimes 来检查调用次数:
beforeEach(()=>{
jest.useFakeTimers()
})
test('timer测试', ()=>{
const fn = jest.fn();
timer(fn);
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1);
})
此外,还有 runOnlyPendingTimers 方法用来执行当前位于队列中的 timers,以及 advanceTimersByTime 方法用来快进 X 毫秒。
例如,在存在嵌套的定时器时,可以通过 advanceTimersByTime 快进来模拟:
// timer.js
export const timerTwice = (callback) => {
setTimeout(() => {
callback();
setTimeout(() => {
callback();
}, 3000);
}, 3000);
};
// timer.test.js
import { timerTwice } from "./timer";
test("timerTwice 测试", () => {
const fn = jest.fn();
timerTwice(fn);
jest.advanceTimersByTime(3000);
expect(fn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(3000);
expect(fn).toHaveBeenCalledTimes(2);
});
无论是模拟网络请求、类还是定时器,Mock 都是一个强大的工具,可以帮助我们构建可靠且高效的测试用例。
snapshot
假设当前存在一个配置,配置的内容可能会经常变更,如下所示:
export const generateConfig = () => {
return {
server: "http://localhost",
port: 8001,
domain: "localhost",
};
};
toEqual 匹配
如果对它进行测试用例编写,最简单的方式就是使用 toEqual 匹配,如下所示:
import { generateConfig } from "./snapshot";
test("测试 generateConfig", () => {
expect(generateConfig()).toEqual({
server: "http://localhost",
port: 8001,
domain: "localhost",
});
});
但是这种方式存在一些问题:每当配置文件发生变更时,都需要修改测试用例。为了避免测试用例频繁修改,可以通过 snapshot 快照来解决这个问题。
toMatchSnapshot
通过 toMatchSnapshot 函数生成快照:
test("测试 generateConfig", () => {
expect(generateConfig()).toMatchSnapshot();
});
第一次执行 toMatchSnapshot 时,会生成一个 __snapshots__ 文件夹,里面存放着 xxx.test.js.snap 这样的文件,内容是当前配置的执行结果。
第二次执行时,会生成一个新的快照并与已有的快照进行比较。如果相同则测试通过;如果不相同,测试用例不通过,并且在命令行会提示你是否需要更新快照,如 “1 snapshot failed from 1 test suite. Inspect your code changes or press u to update them”。
按下 u 键之后,测试用例会通过,并且覆盖原有的快照。
快照的值不同
如果该函数每次的值不同,生成的快照也不相同,例如每次调用函数返回时间戳:
export const generateConfig = () => {
return {
server: "http://localhost",
port: 8002,
domain: "localhost",
date: new Date()
};
};
在这种情况下,toMatchSnapshot 可以接受一个对象作为参数,该对象用于描述快照中的某些字段应该如何匹配:
test("测试 generateConfig", () => {
expect(generateConfig()).toMatchSnapshot({
date: expect.any(Date)
});
});
行内快照
上述的快照是在 __snapshots__ 文件夹下生成的,还有一种方式是通过 toMatchInlineSnapshot 在当前的 .test.js 文件中生成。需要注意的是,这种方式通常需要配合 prettier 工具来使用。
test("测试 generateConfig", () => {
expect(generateConfig()).toMatchInlineSnapshot({
date: expect.any(Date),
});
});
测试用例通过后,该用例的格式如下:
test("测试 generateConfig", () => {
expect(generateConfig()).toMatchInlineSnapshot({
date: expect.any(Date)
}, `
{
"date": Any<Date>,
"domain": "localhost",
"port": 8002,
"server": "http://localhost",
}
`);
});
使用 snapshot 测试可以有效地减少频繁修改测试用例的工作量。无论配置如何变化,只需要更新一次快照即可保持测试的一致性。
本篇及上一篇文章的内容合在一起涵盖了 Jest 的基本使用和高级配置。更多有关前端工程化的内容,请参考我的其他博文,持续更新中~
全面掌握 Jest:从零开始的测试指南(下篇)的更多相关文章
- 《大话移动APP测试:Android与iOS应用测试指南》
<大话移动app测试:android与ios应用测试指南> 基本信息 作者: 陈晔 出版社:清华大学出版社 ISBN:9787302368793 上架时间:2014-7-7 出版日期:20 ...
- 推荐——Monkey《大话 app 测试——Android、iOS 应用测试指南》
<大话移动——Android与iOS应用测试指南> 京东可以预购啦!http://item.jd.com/11495028.html 当当网:http://product.dangdang ...
- OWASP固件安全性测试指南
OWASP固件安全性测试指南 固件安全评估,英文名称 firmware security testing methodology 简称 FSTM.该指导方法主要是为了安全研究人员.软件开发人员.顾问. ...
- Web安全测试指南--文件系统
上传: 编号 Web_FileSys_01 用例名称 上传功能测试 用例描述 测试上传功能是否对上传的文件类型做限制. 严重级别 高 前置条件 1. 目标web应用可访问,业务正常运行. 2. 目 ...
- Web安全测试指南--认证
认证: 5.1.1.敏感数据传输: 编号 Web_Authen_01_01 用例名称 敏感数据传输保密性测试 用例描述 测试敏感数据是否通过加密通道进行传输以防止信息泄漏. 严重级别 高 前置条件 1 ...
- 读书笔记——商广明《Nmap渗透测试指南》
一 Nmap基础学习 1.简介及安装 Nmap是一款由C语言编写的.开源免费的网络发现(Network Discovery)和安全审计(Security Auditing)工具.软件名字Nmap是Ne ...
- Mousejack测试指南
0x00 前言 近日,Bastille的研究团队发现了一种针对蓝牙键盘鼠标的攻击,攻击者可以利用漏洞控制电脑操作,他们将此攻击命名为MouseJack. 攻击者仅需要在亚马逊上以60美元购买设备,改造 ...
- 测试指南(适用于Feature/promotion/bug)
1.提前了解需求,在需求的业务基础和开发的架构基础上分析测试关键点,给出测试策略,甚至需要准备测试数据: 2.分析需求时不要受开发影响,要有自己的分析和判断,包括测试范围,测试时间: 3.在开始测试之 ...
- 微信小程序测试指南
[本文出自天外归云的博客园] 微信小程序本地部署测试方法 下载微信开发者工具 让小程序管理员将测试人员的微信号添加开发者权限 本地设置hosts为测试环境hosts 打开微信web开发者工具并扫码登录 ...
- metasploit渗透测试指南概要整理
一.名词解释 exploit 测试者利用它来攻击一个系统,程序,或服务,以获得开发者意料之外的结果.常见的 有内存溢出,网站程序漏洞利用,配置错误exploit. payload 我们想让被攻击系统执 ...
随机推荐
- 如何解决 CentOS 7 官方 yum 仓库无法使用的问题
一.背景介绍 2024 年 7 月 1 日,在编译基于 CentOS 7.6.1810 镜像的 Dockerfile 过程中,执行 yum install 指令时,遇到了错误:Could not re ...
- 题解:P10417 [蓝桥杯 2023 国 A] 第 K 小的和
分析 这道题不是板子么. 先对序列排序,然后二分答案,设当前答案为 \(x\),枚举 \(a\) 中的数,然后二分查找 \(b\) 中不大于 \(x-a\) 的元素个数,累加判断是否不大于 \(k\) ...
- [rCore学习笔记 00]总览
写在前面 本随笔是非常菜的菜鸡写的.如有问题请及时提出. 可以联系:1160712160@qq.com GitHhub:https://github.com/WindDevil (目前啥也没有 rCo ...
- 机器学习:详解什么是端到端的深度学习?(What is end-to-end deep learning?)
什么是端到端的深度学习? 深度学习中最令人振奋的最新动态之一就是端到端深度学习的兴起,那么端到端学习到底是什么呢?简而言之,以前有一些数据处理系统或者学习系统,它们需要多个阶段的处理.那么端到端深度学 ...
- Nuxt.js必读:轻松掌握运行时配置与 useRuntimeConfig
title: Nuxt.js必读:轻松掌握运行时配置与 useRuntimeConfig date: 2024/7/29 updated: 2024/7/29 author: cmdragon exc ...
- docker容器下安装nccl失败,报错:Failed to init nccl communicator for group,init nccl communicator for group nccl_world_group
相关内容参考: https://www.cnblogs.com/devilmaycry812839668/p/15022320.html =============================== ...
- 必看!S3File Sink Connector 使用文档
S3File 是一个用于管理 Amazon S3(Simple Storage Service)的 Python 模块.当前,Apache SeaTunnel 已经支持 S3File Sink Con ...
- [天线原理及设计>基本原理] 3. 辐射方向图或天线方向图
<Antenna_Theory_Analysis_and_Design_3rd_Constantine_A._Balanis.pdf> 3. 辐射方向图或天线方向图 天线辐射方向图或天线方 ...
- C#模拟键盘输入、键状态和监听键盘消息
模拟键盘输入 模拟键盘输入的功能需要依赖Windows函数实现,这个函数是SendInput,它是专门用来模拟键盘.鼠标等设备输入的函数. 另外和键盘输入相关的函数还有SendKeys,它是Syste ...
- Win32 状态栏用法
WIN32 状态控件用法 1.创建控件 状态栏类名: STATUSCLASSNAME #define STATUSCLASSNAMEW L"msctls_statusbar32 ...