前端如何做单元测试? 看这篇就入门了
前言
对于现在的前端工程,一个标准完整的项目,通常情况单元测试是非常必要的。但很多时候我们只是完成了项目而忽略了项目测试。我认为其中一个很大的原因是很多人对单元测试认知不够,因此我写了这边文章,一方面期望通过这篇文章让你对单元测试有一个初步认识。另一个方面希望通过代码示例,让你掌握写单元测试实践能力。
前端为什么需要单元测试?
必要性:JavaScript 缺少类型检查,编译期间无法定位到错误,单元测试可以帮助你测试多种异常情况。
正确性:测试可以验证代码的正确性,在上线前做到心里有底。
自动化:通过 console 虽然可以打印出内部信息,但是这是一次性的事情,下次测试还需要从头来过,效率不能得到保证。通过编写测试用例,可以做到一次编写,多次运行。
保证重构:互联网行业产品迭代速度很快,迭代后必然存在代码重构的过程,那怎么才能保证重构后代码的质量呢?有测试用例做后盾,就可以大胆的进行重构。
现状
下面是一份抽样调查片段,抽样依据如下:
向 200 名相关者发出在线问卷调查,其中 70 人回答了问卷中的问题,前端人数占 81.16%,如果你有兴趣的话,也可以帮我填一下调查问卷 (https://www.wjx.cn/vm/Ombu9q1.aspx)
数据收集日期:2021.09.21—2021.10.08
目标群体:所有开发人员
组织规模:不到 50 人,50 到 100人, 100人以上
你执行过 JavaScript 单元测试吗?
调查中的另一个有趣的见解是,在大型组织中单元测试更受欢迎。其中一个原因可能是,由于大型组织需要处理大规模的产品,以及频繁的功能迭代吧。这种持续的迭代方式,迫使他们进行自动化测试的投入。更具体地说,单元测试有助于增强产品的整体质量。
另外,报告显示超 80% 人认为单元测试可以有效的提高质量,超 60% 人使用过 Jest 去编写前端单元测试,超 40% 的人认为单元测试覆盖率是重要的且覆盖率应该大于 80%。
常见单元测试工具
目前用的最多的前端单元测试框架主要有 Mocha (https://mochajs.cn/)、Jest (https://www.jestjs.cn/),但我推荐你使用 Jest,因为 Jest 和 Mocha 相比,无论从 github starts & issues 量,npm下载量相比,都有明显优势。
github stars 以及 npm 下载量的实时数据,参见:jest vs mocha (https://www.npmtrends.com/jest-vs-mocha) 截图日期为 2021.11.25
Github stars & issues
npm 下载量
Jest 的下载量较大,一部分原因是因为 create-react-app 脚手架默认内置了 Jest, 而大部分 react 项目都是用它生成的。
从 github starts & issues 以及 npm 下载量角度来看,Jest 的关注度更高,社区也更活跃
框架对比
Mocha 生态好,但是需要较多的配置来实现高扩展性
Jest 开箱即用
比如对 sum 函数写用例
./sum.js
function sum(a, b) {return a + b;}module.exports = sum;
Mocha + Chai 方式
Mocha 需要引入 chai 或则其他断言库去断言, 如果你需要查看覆盖率报告你还需要安装 nyc 或者其他覆盖率工具
./test/sum.test.js
const { expect, assert } = require('chai');const sum = require('../sum');describe('sum', function() {it('adds 1 + 2 to equal 3', () => {assert(sum(1, 2) === 3);});});
Jest 方式
Jest 默认支持断言,同时默认支持覆盖率测试
./test/sum.test.js
const sum = require('./sum');describe('sum function test', () => {it('sum(1, 2) === 3', () => {expect(sum(1, 2)).toBe(3);});// 这里 test 和 it 没有明显区别,it 是指: it should xxx, test 是指 test xxxtest('sum(1, 2) === 3', () => {expect(sum(1, 2)).toBe(3);});})
可见无论是受欢迎度和写法上,Jest 都有很大的优势,因此推荐你使用开箱即用的 Jest
如何开始?
1.安装依赖
npm install --save-dev jest
2.简单的例子
首先,创建一个 sum.js 文件
./sum.js
function sum(a, b) {return a + b;}module.exports = sum;
创建一个名为 sum.test.js 的文件,这个文件包含了实际测试内容:
./test/sum.test.js
const sum = require('../sum');test('adds 1 + 2 to equal 3', () => {expect(sum(1, 2)).toBe(3);});
将下面的配置部分添加到你的 package.json 里面
{"scripts": {"test": "jest"},}
运行 npm run test ,jest 将打印下面这个消息
3.不支持部分 ES6 语法
nodejs 采用的是 CommonJS 的模块化规范,使用 require 引入模块;而 import 是 ES6 的模块化规范关键字。想要使用 import,必须引入 babel 转义支持,通过 babel 进行编译,使其变成 node 的模块化代码
如以下文件改写成 ES6 写法后,运行 npm run test将会报错
./sum.js
export function sum(a, b) {return a + b;}
./test/sum.test.js
import { sum } from '../sum';test('adds 1 + 2 to equal 3', () => {expect(sum(1, 2)).toBe(3);});
为了能使用这些新特性,我们就需要使用 babel 把 ES6 转成 ES5 语法
解决办法
安装依赖
npm install --save-dev @babel/core @babel/preset-env
根目录加入.babelrc
{ "presets": ["@babel/preset-env"] }
再次运行 npm run test ,问题解决
原理
jest 运行时内部先执行( jest-babel ),检测是否安装 babel-core,然后取 .babelrc 中的配置运行测试之前结合 babel 先把测试用例代码转换一遍然后再进行测试
4.测试 ts 文件
jest 需要借助 .babelrc 去解析 TypeScript 文件再进行测试
安装依赖
npm install --save-dev @babel/preset-typescript
**改写 **.babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-typescript"] }
为了解决编辑器对 jest 断言方法的类型报错,如 test、expect 的报错,你还需要安装
npm install --save-dev @types/jest
./get.ts
/*** 访问嵌套对象,避免代码中出现类似 user && user.personalInfo ? user.personalInfo.name : null 的代码*/export function get<T>(object: any, path: Array<number | string>, defaultValue?: T) : T {const result = path.reduce((obj, key) => obj !== undefined ? obj[key] : undefined, object);return result !== undefined ? result : defaultValue;}
./test/get.test.ts
import { get } from './get';test('测试嵌套对象存在的可枚举属性 line1', () => {expect(get({id: 101,email: 'jack@dev.com',personalInfo: {name: 'Jack',address: {line1: 'westwish st',line2: 'washmasher',city: 'wallas',state: 'WX'}}}, ['personalInfo', 'address', 'line1'])).toBe('westwish st');});
运行 npm run test
5.持续监听
为了提高效率,可以通过加启动参数的方式让 jest 持续监听文件的修改,而不需要每次修改完再重新执行测试用例
改写 package.json
"scripts": { "test": "jest --watchAll" },
效果
5.生成测试覆盖率报告
什么是单元测试覆盖率?
单元测试覆盖率是一种软件测试的度量指标,指在所有功能代码中,完成了单元测试的代码所占的比例。有很多自动化测试框架工具可以提供这一统计数据,其中最基础的计算方式为:
单元测试覆盖率 = 被测代码行数 / 参测代码总行数 * 100%
如何生成?
加入 jest.config.js 文件
module.exports = {// 是否显示覆盖率报告collectCoverage: true,// 告诉 jest 哪些文件需要经过单元测试测试collectCoverageFrom: ['get.ts', 'sum.ts', 'src/utils/**/*'],}
再次运行效果
参数解读
设置单元测试覆盖率阀值
个人认为既然在项目中集成了单元测试,那么非常有必要关注单元测试的质量,而覆盖率则一定程度上客观的反映了单测的质量,同时我们还可以通过设置单元测试阀值的方式提示用户是否达到了预期质量。
jest.config.js 文件
module.exports = {collectCoverage: true, // 是否显示覆盖率报告collectCoverageFrom: ['get.ts', 'sum.ts', 'src/utils/**/*'], // 告诉 jest 哪些文件需要经过单元测试测试coverageThreshold: {global: {statements: 90, // 保证每个语句都执行了functions: 90, // 保证每个函数都调用了branches: 90, // 保证每个 if 等分支代码都执行了},},
上述阀值要求我们的测试用例足够充分,如果我们的用例没有足够充分,则下面的报错将会帮助你去完善
6.如何编写单元测试
下面我们以 fetchEnv 方法作为案例,编写一套完整的单元测试用例供读者参考
编写 fetchEnv 方法
./src/utils/fetchEnv.ts 文件
/*** 环境参数枚举*/enum IEnvEnum {DEV = 'dev', // 开发TEST = 'test', // 测试PRE = 'pre', // 预发PROD = 'prod', // 生产}/*** 根据链接获取当前环境参数* @param {string?} url 资源链接* @returns {IEnvEnum} 环境参数*/export function fetchEnv(url: string): IEnvEnum {const envs = [IEnvEnum.DEV, IEnvEnum.TEST, IEnvEnum.PRE];return envs.find((env) => url.includes(env)) || IEnvEnum.PROD;}
编写对应的单元测试
./test/fetchEnv.test.ts 文件
import { fetchEnv } from '../src/utils/fetchEnv';describe('fetchEnv', () => {it ('判断是否 dev 环境', () => {expect(fetchEnv('https://www.imooc.dev.com/')).toBe('dev');});it ('判断是否 test 环境', () => {expect(fetchEnv('https://www.imooc.test.com/')).toBe('test');});it ('判断是否 pre 环境', () => {expect(fetchEnv('https://www.imooc.pre.com/')).toBe('pre');});it ('判断是否 prod 环境', () => {expect(fetchEnv('https://www.imooc.prod.com/')).toBe('prod');});it ('判断是否 prod 环境', () => {expect(fetchEnv('https://www.imooc.com/')).toBe('prod');});});
执行结果
7.常用断言方法
关于断言方法有很多,这里仅摘出常用方法,如果你想了解更多,你可以去 Jest 官网 API (https://www.jestjs.cn/docs/expect) 部分查看
.not 修饰符允许你测试结果不等于某个值的情况
./test/sum.test.js
import { sum } from './sum';test('sum(2, 4) 不等于 5', () => {expect(sum(2, 4)).not.toBe(5);})
./src/utils/userInfo.js
export const getUserInfo = () => {return {name: 'moji',age: 24,}}
./test/userInfo.test.js
import { getUserInfo } from '../src/userInfo.js';test('getUserInfo()返回的对象深度相等', () => {expect(getUserInfo()).toEqual(getUserInfo());})test('getUserInfo()返回的对象内存地址不同', () => {expect(getUserInfo()).not.toBe(getUserInfo());})
.toHaveLength 可以很方便的用来测试字符串和数组类型的长度是否满足预期
./src/utils/getIntArray.js
export const getIntArray = (num) => {if (!Number.isInteger(num)) {throw Error('"getIntArray"只接受整数类型的参数');}return [...new Array(num).keys()];};
./test/getIntArray.test.js
./test/getIntArray.test.jsimport { getIntArray } from '../src/utils/getIntArray';test('getIntArray(3)返回的数组长度应该为3', () => {expect(getIntArray(3)).toHaveLength(3);})
.toThorw 能够让我们测试被测试方法是否按照预期抛出异常
但是需要注意的是:我们必须使用一个函数将被测试的函数做一个包装,正如下面 getIntArrayWrapFn 所做的那样,否则会因为函数抛出错误导致该断言失败。./test/getIntArray.test.js
import { getIntArray } from '../src/utils/getIntArray';test('getIntArray(3.3)应该抛出错误', () => {function getIntArrayWrapFn() {getIntArray(3.3);}expect(getIntArrayWrapFn).toThrow('"getIntArray"只接受整数类型的参数');})
.toMatch 传入一个正则表达式,它允许我们来进行字符串类型的正则匹配
./test/userInfo.test.js
import { getUserInfo } from '../src/utils/userInfo.js';test("getUserInfo().name 应该包含'mo'", () => {expect(getUserInfo().name).toMatch(/mo/i);})
测试异步函数
./servers/fetchUser.js
/*** 获取用户信息*/export const fetchUser = () => {return new Promise((resole) => {setTimeout(() => {resole({name: 'moji',age: 24,})}, 2000)})}
./test/fetchUser.test.js
import { fetchUser } from '../src/fetchUser';test('fetchUser() 可以请求到一个用户名字为 moji', async () => {const data = await fetchUser();expect(data.name).toBe('moji')})
这里你可能看到这样一条报错
这是因为 @babel/preset-env 不支持 async await 导致的,这时候就需要对 babel 配置进行增强,可以安装 @babel/plugin-transform-runtime 这个插件解决
npm install --save-dev @babel/plugin-transform-runtime
同时改写 .babelrc
{"presets": ["@babel/preset-env", "@babel/preset-typescript"],"plugins": ["@babel/plugin-transform-runtime"]}
再次运行就不会出现报错了
.toContain 匹配对象中是否包含
./test/toContain.test.js
const names = ['liam', 'jim', 'bart'];test('匹配对象是否包含', () => {expect(names).toContain('jim');})
检查一些特殊的值(null,undefined 和 boolean)
toBeNull 仅匹配 nulltoBeUndefined 仅匹配 undefinedtoBeDefined 与…相反 toBeUndefinedtoBeTruthy 匹配 if 语句视为 true 的任何内容toBeFalsy 匹配 if 语句视为 false 的任何内容检查数字类型(number)toBeGreaterThan 大于toBeGreaterThanOrEqual 至少(大于等于)toBeLessThan 小于toBeLessThanOrEqual 最多(小于等于)toBeCloseTo 用来匹配浮点数(带小数点的相等)
总结
以上就是文章全部内容,相信你阅读完这篇文章后,已经掌握了前端单元测试的基本知识,甚至可以按照文章教学步骤,现在就可以在你的项目中接入单元测试。同时在阅读过程中如果你有任何问题,或者有更好见解,更好的框架推荐,欢迎你在评论区留言!
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!
相关文章:

前端如何做单元测试? 看这篇就入门了
前言 对于现在的前端工程,一个标准完整的项目,通常情况单元测试是非常必要的。但很多时候我们只是完成了项目而忽略了项目测试。我认为其中一个很大的原因是很多人对单元测试认知不够,因此我写了这边文章,一方面期望通过这篇文章…...

Chainlit快速实现AI对话应用的聊天记录如何持久性保存
前言 Chainlit 可以设置聊天记录用户搜索和浏览过去的对话。 如何实现 要启用聊天历史记录,您需要启用: 数据持久性身份验证恢复对话 为了让用户继续持久对话,请使用cl.on_chat_resume 生命周期钩子 装饰器使用户能够继续对话。需要同时启用数据持久性和身份验证。 该…...

【探索数据结构与算法】——深入了解双向链表(图文详解)
目录 一、双向链表的基本概念 二、双向链表的结构 三、双向链表的基本操作实现方法 1.双向链表的初始化 2.双向链表的头插 3.双向链表的尾插 6.查找节点 7.在指定位置之前插入节点 8.删除指定位置节点 9.打印链表数据 10.双向链表销毁 四、完整代码实现 …...
linux常用命令备忘录
一、常用命令 查看被占用进程:ps ef|grep 11612 查看当前目录:pwd 查看文件的md5: (linux)md5sum 文件名 (windows)certutil -hashfile some_file MD5 查看当前目录的文件大小:…...

【C++进阶学习】第十二弹——C++ 异常处理:深入解析与实践应用
前言: 在C编程语言中,异常处理是一种重要的机制,它允许程序员在运行时捕获和处理错误或异常情况。本文将详细介绍C异常处理的相关知识点,包括异常的定义、抛出与捕获、异常处理的原则、以及在实际编程中的应用。 目录 1. 异常处理…...
《算法竞赛进阶指南》0x23剪枝
剪枝,就是减少搜索树的规模、尽可能排除搜索书中不必要的分支的一种手段。形象地看,就好像剪掉了搜索树的枝条,故被称为“剪枝”。在深度优先搜索中,有以下常见的剪枝方法。 1.优化搜索顺序 在一些搜索问题中,搜索树的…...

同态加密和SEAL库的介绍(三)BFV - Batch Encoder
写在前面: 在上一篇中展示了如何使用 BFV 方案执行一个非常简单的计算。该计算在 plain_modulus 参数下进行,并且仅使用了 BFV 明文多项式中的一个系数。这种方法有两个显著的问题: 实际应用通常使用整数或实数运算,而不是模运算…...

Docker 环境下使用 Traefik v3 和 MinIO 快速搭建私有化对象存储服务
上一篇文章中,我们使用 Traefik 新版本完成了本地服务网关的搭建。接下来,来使用 Traefik 的能力,进行一系列相关的基础设施搭建吧。 本篇文章,聊聊 MinIO 的单独使用,以及结合 Traefik 完成私有化 S3 服务的基础搭建…...

玛雅房产系统源码开发与技术功能解析
引言 随着房地产市场的蓬勃发展,房产管理系统(Real Estate Management System, REMS)作为提升行业效率、优化资源配置的关键工具,其重要性日益凸显。房产系统源码开发不仅涉及复杂的业务逻辑处理,还融合了先进的软件开…...

c++----初识模板
大家好,这篇博客想与大家分享一些我们c中比较好用的知识点。模板。首先咧,我们都知道模板嘛,就是以前人的经验总结出来的知识。方便我们使用。这里的模板也是一样的。当我们学习过后,对于一些在c中的自定义函数,我们在…...

SpringBoot3热部署
引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional> </dependency> 默认就是,无需配置 可以了…...

J. 二进制与、平方和
https://codeforces.com/gym/104095/problem/J 分析操作一 1&00 ,0&10,ai<qmi(2,24),说明每个数最多操作25次 维护区间或和,orsum & x orsum 就不用递归下去了 势能线段树code // Problem: J. 二进制与、平方和 // Contest: Codeforc…...

LVS中NAT模式和DR模式实战讲解
1DR模式 DR:Direct Routing,直接路由,LVS默认模式,应用最广泛,通过为请求报文重新封装一个MAC首部进行 转发,源MAC是DIP所在的接口的MAC,目标MAC是某挑选出的RS的RIP所在接口的MAC地址;源 IP/PORT…...

写给小白程序员的一封信
文章目录 1.编程小白如何成为大神?大学新生的最佳入门攻略2.程序员的练级攻略3.编程语言的选择4.熟悉Linux5.学会git6.知道在哪寻求帮助7.多结交朋友8.参加开源项目9.坚持下去 1.编程小白如何成为大神?大学新生的最佳入门攻略 编程已成为当代大学生的必…...

Leaf分布式ID
文章目录 系统对Id号的要求UUIDsnowflakeLeafLeaf-snowflakeLeaf-segmentMySQL自增主键segment双buffer 系统对Id号的要求 1、业务 1)全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求 2)趋势递增&a…...

Starrocks解析json数组
json数据 [{"spec": "70g/支","unit": "支","skuId": "1707823848651276346","amount": 6,"weight": 70,"spuName": "伊利 甄稀 苦咖啡味雪糕 流心冰淇淋 70g/支",&quo…...

安卓基本布局(下)
TableLayout 常用属性描述collapseColumns设置需要被隐藏的列的列号。shrinkColumns设置允许被伸缩的列的列号。stretchColumns设置允许被拉伸的列的列号。 <TableLayout xmlns:android"http://schemas.android.com/apk/res/android"android:id"id/TableL…...

Python中使用正则表达式
摘要: 正则表达式,又称为规则表达式,它不是某种编程语言所特有的,而是计算机科学的一个概念,通常被用来检索和替换某些规则的文本。 一.正则表达式的语法 ①行定位符 行定位符就是用来描述字符串的边界。"^&qu…...

三大口诀不一样的代码,小小的制表符和换行符玩的溜呀
# 小案例,打印输出加法口诀 for i in range(1,10):for j in range(1,10):if j>i:breakprint(f"{j}{i}{ji}".strip(),end\t)print() print(\n) for i in range(1,10):for j in range(1,10):if j>i:breakprint(f"{j}x{i}{j*i}",end\t)print…...
[qt] 线程等待与唤醒
对于生产者与消费者的数据处理的另一种好的解决方法是使用QWaitCondition类,允许线程在一定的条件下唤醒其他多个线程来共同处理。 一 定义公共变量 DataSize: 生产者生产数据的大小BufferSize: 也就是这个缓冲区的大小,每个单元是一个int,也有可能是一个链表,结构…...

jenkins集成gitlab发布到远程服务器
jenkins集成gitlab发布到远程服务器 前面我们讲了通过创建maven项目部署在jenkins本地服务器,这次实验我们将部署在远程服务器,再以nginx作为前端项目做一个小小的举例 1、部署nginx服务 [rootweb ~]# docker pull nginx [rootweb ~]# docker images …...

检测到 #include 错误。请更新 includePath。已为此翻译单元(D:\软件\vscode\test.c)禁用波形曲线
原文链接:【VScodeMinGw】安装配置教程 下载mingw64 打开可以看到bin文件夹下是多个.exe文件,gcc.exe地址在环境配置中要用到 原文链接:VSCode中出现“#include错误,请更新includePath“问题,解决方法 重新VScode后…...
An improved YOLACT algorithm for instance segmentation of stacking parts
【一种用于堆叠零件实例分割的改进 YOLACT 算法】 摘要 实例分割在众多应用场景中均是一项至关重要的任务。对于计算机视觉而言,堆叠物体的实例分割是一项挑战。为应对这一挑战,我们提出了一种改进的 YOLACT(You Only Look At CoefficienTs)算法。为提高密集堆叠场景下特…...

11.RV1126-ROCKX项目 API和人脸检测画框
一.ROCKX的API 1.ROCKX的作用 ROCKX的AI组件可以快速搭建 AI的应用,这些应用可以是车牌识别、人脸识别、目标识别,人体骨骼识别等等。主要用于各种检测识别。例如下图: 2.ROCKX人脸识别的API rockx_ret_t rockx_create(rockx_handle_t *han…...
n皇后问题的 C++ 回溯算法教学攻略
一、问题描述 n皇后问题是经典的回溯算法问题。给定一个 nn 的棋盘,要求在棋盘上放置 n 个皇后,使得任何两个皇后之间不能互相攻击。皇后可以攻击同一行、同一列以及同一对角线上的棋子。我们需要找出所有的合法放置方案并输出方案数。 二、输入输出形…...

SSIM、PSNR、LPIPS、MUSIQ、NRQM、NIQE 六个图像质量评估指标
评价指标 1. SSIM(Structural Similarity Index) 📌 定义 结构相似性指数(Structural Similarality Index)是一种衡量两幅图像相似性的指标,考虑了亮度、对比度和结构信息的相似性,比传统的 P…...

2025服装收银系统推荐:智能管理助力服装商家高效经营
在服装批发零售行业,一套高效的收银系统不仅能简化日常经营流程,还能通过数据分析帮助商家优化库存、提升销售。随着AI技术的普及,现代收银系统已不再局限于简单的记账功能,而是能提供智能选品、库存预警、精准营销等进阶服务。 …...
上门服务小程序订单系统框架设计
一、逻辑分析 上门服务小程序订单系统主要涉及服务展示、用户下单、订单处理、服务人员接单与服务完成反馈等核心流程。 服务展示:不同类型的上门服务(如家政、维修等)需要在小程序中展示详细信息,包括服务名称、价格、服务内容介…...

HTML5+CSS3+JS小实例:具有粘性重力的磨砂玻璃导航栏
实例:具有粘性重力的磨砂玻璃导航栏 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width…...

AudioRelay 0.27.5 手机充当电脑音响
—————【下 载 地 址】——————— 【本章下载一】:https://pan.xunlei.com/s/VOS4MvfPxrnfS2Zu_YS4egykA1?pwdi2we# 【本章下载二】:https://pan.xunlei.com/s/VOS4MvfPxrnfS2Zu_YS4egykA1?pwdi2we# 【百款黑科技】:https://uc…...