全面掌握 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 中定义回调函数,或者通过 mockReturnValuemockReturnValueOnce 方法定义返回值。

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);});
});

也可以直接通过 resolvesrejects 获取响应的所有参数并进行匹配:

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 的基本使用和高级配置。更多有关前端工程化的内容,请参考我的其他博文,持续更新中~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1537473.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

list从0到1的突破

目录 前言 1.list的介绍 2.list的常见接口 2.1 构造函数&#xff08; (constructor)&#xff09; 接口说明 2.2 list iterator 的使用 2.3 list capacity 2.4 list element access 2.5 list modifiers 3.list的迭代器失效 附整套练习源码 结束语 前言 前面我们学习…

一款源码阅读的插件

文章目录 进度汇报功能预览添加高亮标记高亮风格设置笔记颜色设置数据概览高亮数据详情 结尾 进度汇报 之前提到最近有在开发一个源码阅读的IDEA插件&#xff0c;第一版已经开发完上传插件市场了&#xff0c;等官方审批通过就可以尝鲜了。插件名称&#xff1a;Mark source cod…

防火墙——NAT

目录 NAT NAT分类 旧分类 新分类 NAT配置 源NAT​编辑 配置源NAT地址池​编辑 关于源NAT环路问题 环境如下​编辑 防火墙nat​编辑​编辑 路由器要配置指向11.0.0.0 网段的静态路由​编辑 测试​编辑 如果此时有外网用户直接pingNAT地址&#xff0c;则环路出现。​…

PAT甲级-1016 Phone Bills

题目 题目大意 顾客打长途电话计费&#xff0c;输出每月的账单。输入一行给出一天24小时的计费钱数&#xff0c;注意单位是美分&#xff0c;还要乘以0.01。接下来给出n条记录&#xff0c;每条记录都包括客户名&#xff0c;时间&#xff0c;状态。“on-line”是开始打电话的时间…

专题四_位运算( >> , << , , | , ^ )_算法详细总结

目录 位运算 常见位运算总结 1.基础位运算 2.给一个数 n ,确定它的二进制表示中的第 x 位是 0 还是 1 3.运算符的优先级 4.将一个数 n 的二进制表示的第 x 位修改成 1 5.将一个数n的二进制表示的第x位修改成0 6.位图的思想 7.提取一个数&#xff08;n&#xff09;二进…

如何优雅地处理返回值

我们已经知道了如何优雅的校验传入的参数了&#xff0c;那么后端服务器如何实现把数据返回给前端呢&#xff1f; 返回格式 后端返回给前端我们一般用 JSON 体方式&#xff0c;定义如下&#xff1a; {#返回状态码code:string, #返回信息描述message:string,#返回值data…

算法设计与分析(线性时间选择算法

目录 线性时间选择算法&#xff08;QuickSelect&#xff09;实现注意事项有可能出现的特殊情况&#xff1a;小结&#xff1a; 线性时间选择算法&#xff08;QuickSelect&#xff09;实现 线性时间选择算法 是快速排序算法的一个变种&#xff0c;用于在未完全排序的数组中找到第…

Next-ViT: 下一代视觉Transformer,用于现实工业场景中的高效部署

摘要 由于复杂的注意力机制和模型设计&#xff0c;大多数现有的视觉Transformer&#xff08;ViTs&#xff09;在实际的工业部署场景中&#xff0c;如TensorRT和CoreML&#xff0c;无法像卷积神经网络&#xff08;CNNs&#xff09;那样高效运行。这提出了一个明显的挑战&#x…

[Redis] Redis中的set和zset类型

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

微信,手机文件管理,通过自己软件打开——手机平板电脑编程———未来之窗行业应用跨平台架构

一、手机平板IT人员编程编辑器 专为 IT 和运维人员设计的手机和平板编程编辑器&#xff0c;具有便携灵活、即时响应、适应多场景、触控便捷、资源丰富、成本较低、激发创意和数据同步方便等优点。 二、手机平板现状 目前手机和平板的现状是缺乏专门针对 IT 人员的编辑工具&a…

避免服务器安装多个mysql引起冲突的安装方法

最近工作中涉及到了数据迁移的工作. 需要升级mysql版本到8.4.2为了避免升级后服务出现异常, 因此需要保留原来的mysql,所以会出现一台服务器上运行两个mysql的情况 mysql并不陌生, 但是安装不当很容易引起服务配置文件的冲突,导致服务不可用, 今天就来介绍一种可以完美避免冲突…

COMDEL电源CX2500S RF13.56MHZ RF GENERATOR手侧

COMDEL电源CX2500S RF13.56MHZ RF GENERATOR手侧

【C++ Primer Plus习题】16.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream> #include <list> using …

采用 Redis+数据库为某互联网文化公司建立网上社区平台

目录 案例 【说明】 【问题 1】(10 分) 【问题 2】(7 分) 【问题 3】(8 分) 【答案】 【问题 1】解析 【问题 3】解析 相关推荐 案例 阅读以下关于数据库缓存的叙述&#xff0c;回答问题 1 至问题 3。 【说明】 某互联网文化发展公司因业务发展&#xff0c;需要建立网…

海思Hi3559av100 sdk开发环境搭建

SDK阐释 海思官方给的sdk布局&#xff0c;如Hi3559AV100R001C02SPC031&#xff0c;其包含编译工具、硬件设计资料、软件sdk、文档等资料&#xff0c;tree布局可以构建如下形式&#xff0c;但不是必要的。 软件sdk在 01.software中&#xff0c;这个路径下才是真正的软件代码&…

嵌入式DCMI摄像头功能调试方法

STM32F407芯片带有DCMI接口,在我们的核心板上已经将接口用18PIN的FPC座子引出。 这个接口可以接我们的OV2640接口。 本节我们开始调试摄像头。 16.1. DCMI DCMI接口是ST自己定义的接口。 Digital camera interface (DCMI),是意法半导体公司产品STM32F4xx系列芯片的快速摄像头…

Redis 篇-初步了解 Redis 持久化、Redis 主从集群、Redis 哨兵集群、Redis 分片集群

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 分布式缓存概述 2.0 Redis 持久化 2.1 RDB 持久化 2.1.1 RDB 的 fork 原理 2.2 AOF 持久化 2.3 RDB 与 AOF 之间的区别 3.0 Redis 主从集群 3.1 搭建主从集群 3.2…

使用Tortoisegit完成基于Git提交日志的代码合并

前言 日常开发中除了分支merge合并外&#xff0c;经常会用cherry-pick&#xff0c;示例&#xff1a;git cherry-pick 29d9493d,如果要进行多次代码的遴选&#xff0c;可以借助git工具TortoixeGit&#xff0c;进行多次提交的遴选。 一、Git工具及常用命令 TortoiseGit工具 T…

第二十六篇——九地篇:九种形势的应对之道

目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么&#xff1f; 四、总结五、升华 一、背景介绍 地势的维度重新阐述了懂得人心的重要性&#xff0c;道久其归一为为别人。…

Village Exteriors Kit 中世纪乡村房屋场景模型

此模块化工具包就是你一直在寻找的适合建造所有中世纪幻想村庄和城市建筑所需要的工具包。 皇家园区 - 村庄外饰套件的模型和纹理插件资源包 酒馆和客栈、魔法商店、市政大厅、公会大厅、布莱克史密斯锻造厂、百货商店、珠宝商店、药店、草药师、银行、铠甲、弗莱切、马厩、桌…