随着JavaScript在前后端开发中的广泛应用,测试已成为保证代码质量的关键环节。

为什么需要单元测试

在我们的开发过程中,经常需要定义一些算法函数,例如将接口返回的数据转换成UI组件所需的格式。为了校验这些算法函数的健壮性,部分开发同学可能会手动定义几个输入样本进行初步校验,一旦校验通过便不再深究。

然而,这样的做法可能会带来一些潜在的问题。首先,边界值的情况往往容易被忽视,导致校验不够全面,增加了系统出现故障的风险。其次,随着需求的变化和演进,算法函数可能需要进行优化和扩展。如果前期的校验工作不够彻底,不了解现有函数覆盖的具体场景,就可能导致在后续的修改中引入新的问题。

单元测试可以有效地解决上述问题。在定义算法函数时,同步创建单元测试文件,并将可能出现的各种场景逐一列举。如果单元测试未能通过,项目在编译时会直接报错,从而能够及时发现并针对性地解决问题。此外,当后续有新同学加入并需要扩展功能时,他们不仅需要在原有的单元测试基础上添加新的测试用例,还能确保新功能的正确性,同时保障原有功能的正常运行。

自定义测试逻辑

在开始使用工具来进行单元测试之前,我们可以先自定义一个工具函数供测试使用。

例如,我们有一个 add 函数,期望它能够正确计算两个数的和,并验证其结果是否符合预期。比如,我们希望验证 2 + 3的结果是否等于 5 ,可以使用 expect(add(2, 3)).toBe(5) 这样的代码来实现。为此,我们可以自行定义一个expect 函数,使其具备类似Jest中 expect 函数的功能

function add(a, b) { return a + b; }
function expect(result) {
  return {
    toBe(value) {
      if (result === value) {
        console.log("验证成功");
      } else {
        throw new Error(`执行错误:${result} !== ${value}`);
      }
    },
  };
} // 调用示例
try {
  expect(add(2, 3)).toBe(5);  // 输出:"验证成功"
  expect(add(2, 3)).toBe(6);  // 抛出错误
} catch (err) {
  console.error(err.message);  // 输出:"执行错误:5 !== 6"
}

为了使测试更具描述性和可读性,我们可以进一步增强我们的测试逻辑。例如,我们可以添加一个 test 函数,用于描述测试的目的,并在测试失败时提供更详细的错误信息。

function test(description, fn) {
  try {
    fn();
    console.log(`测试通过: ${description}`);
  } catch (err) {
    console.error(`测试失败: ${description} - ${err.message}`);
  }
}
// 调用示例
test("验证 2 + 3 是否等于 5", () => {
  expect(add(2, 3)).toBe(5);
});
test("验证 2 + 3 是否等于 6", () => {
  expect(add(2, 3)).toBe(6);
});

通过这种方式,我们模拟了一个简单的测试用例,其中 testexpect 函数类似于Jest中的功能。然而,我们的自定义版本相对简陋,缺乏 Jest 提供的丰富功能。

Jest

通过上述示例,我们可以了解到编写测试的基本思路和方法。然而,在实际开发中,我们需要一个功能更加强大、易用性更高的测试工具。Jest 正是这样一个工具,它不仅提供了丰富的匹配器(如toBe、toEqual等),还支持异步测试Mock函数Snapshot测试 等功能。

引入 Jest 的依赖后,我们可以直接使用其内置的 testexpect 函数,从而大大提高测试的效率和准确性。Jest 的强大之处在于它能够帮助我们全面地覆盖各种测试场景,并提供详细的错误报告,使我们能够快速定位和解决问题。

初始化

首先,我们通过 npm install jest -D 安装 Jest 依赖,然后执行 npx jest --init。此时,命令行工具会出现一系列交互式问答,询问你是否要为 Jest 添加名为 test 的脚本指令、是否使用 TypeScript 作为配置文件、测试用例执行环境、是否需要代码覆盖率测试报告、生成测试报告的平台的编译器以及是否需要在每次测试用例执行前重置 Mock 函数状态。

完成所有问答后,Jest 会修改 package.json 文件,并生成jest.config.js配置文件。在执行测试用例时,将依据这些配置项进行。

我们创建一个 math.test.js 文件,并将之前的测试代码放入其中

function add(a, b) {
  return a + b;
}
test("测试 add 函数", () => {
  expect(add(2, 3)).toBe(5);
});

通过 npm run test 执行 Jest 运行指令,可以在命令行工具查看详细的测试信息,包括哪个文件的哪条测试用例的状态,以及简易的测试覆盖率报告。

在实际使用场景中,add 函数通常定义在项目文件中,并通过 ES 模块化 (export 和 import) 方式导出和导入。默认情况下,Jest 并不支持 ES 模块化语法,因此我们需要通过 Babel 进行配置。

首先,执行以下命令安装 Babel 及其核心库和预设

npm install @babel/core @babel/preset-env --save-dev

然后,创建babel.config.js文件并定义配置

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          node: "current",
        },
      },
    ],
  ],
};

接着,将 add 函数移到 math.js 文件中,并使用 export 导出

// math.js
export function add(a, b) {
  return a + b;
}

最后,在 math.test.js 文件中使用 import 导入

// math.test.js
import { add } from './math';
test("测试 add 函数", () => {
  expect(add(2, 3)).toBe(5);
});

通过以上步骤,你就完成了使用 Jest 执行 ES 模块化代码的环境初始化。

匹配器

Jest 中最常用的功能之一就是匹配器。在前面进行测试时,我们就接触过 toBe 这一匹配器,它用于判断值是否相等。除此之外,还有许多其他类型的匹配器。

值相等

判断值相等有两种匹配器:toBetoEqual。对于基本数据类型(如字符串、数字、布尔值),两者的使用效果相同。但对于引用类型(如对象和数组),toBe 只有在两个引用指向同一个内存地址时才会返回 true

const user = { name: "alice" };
const info = { name: "alice" }; test("toEqual", () => {
  expect(info).toEqual(user); // 通过,两者结构相同
});
test("toBe", () => {
  expect(info).toBe(user); // 不通过,两者的引用地址不同
});
是否有值

存在 toBeNulltoBeUndefinedtoBeDefined 匹配器来分别判断值是否为 null、未定义或已定义。

test("toBeNull", () => {
  expect(null).toBeNull();
  expect(0).toBeNull(); // 不通过
  expect("hello").toBeNull(); // 不通过
  expect(undefined).toBeBull(); // 不通过
}); test("toBeUnDefined", () => {
  expect(null).toBeUndefined(); // 不通过
  expect(0).toBeUndefined(); // 不通过
  expect("hello").toBeUndefined(); // 不通过
  expect(undefined).toBeUndefined();
}); test("toBeDefined", () => {
  expect(null).toBeDefined();
  expect(0).toBeDefined();
  expect("hello").toBeDefined();
  expect(undefined).toBeDefined(); // 不通过
});
是否为真

toBeTruthy 用于判断值是否为真,toBeFalsy 用于判断值是否为假,not 用于取反。

test("toBeTruthy", () => {
  expect(null).toBeTruthy(); // 不通过
  expect(0).toBeTruthy(); // 不通过
  expect(1).toBeTruthy();
  expect("").toBeTruthy(); // 不通过
  expect("hello").toBeTruthy();
  expect(undefined).toBeTruthy(); // 不通过
});
test("toBeFalsy", () => {
  expect(null).toBeFalsy();
  expect(0).toBeFalsy();
  expect(1).toBeFalsy(); // 不通过
  expect("").toBeFalsy();
  expect("hello").toBeFalsy(); // 不通过
  expect(undefined).toBeFalsy();
});
test("not", () => {
  expect(null).not.toBeTruthy();
  expect("hello").not.toBeTruthy(); // 不通过
});
数字比较

toBeGreaterThan 用于判断是否大于某个数值,toBeLessThan 用于判断是否小于某个数值,toBeGreaterThanOrEqual 用于判断是否大于或等于某个数值,toBeCloseTo 用于判断是否接近某个数值(差值 < 0.005)。

test("toBeGreaterThan", () => {
  expect(9).toBeGreaterThan(5);
  expect(5).toBeGreaterThan(5); // 不通过
  expect(1).toBeGreaterThan(5); // 不通过
}); test("toBeLessThan", () => {
  expect(9).toBeLessThan(5); // 不通过
  expect(5).toBeLessThan(5); // 不通过
  expect(1).toBeLessThan(5);
}); test("toBeGreaterThanOrEqual", () => {
  expect(9).toBeGreaterThanOrEqual(5);
  expect(5).toBeGreaterThanOrEqual(5);
  expect(1).toBeGreaterThanOrEqual(5); // 不通过
}); test("toBeCloseTo", () => {
  expect(0.1 + 0.2).toBeCloseTo(0.3);
  expect(1 + 2).toBeCloseTo(3);
  expect(0.1 + 0.2).toBeCloseTo(0.4); // 不通过
});
字符串相关

toMatch 用于判断字符串是否包含指定子字符串,部分包含即可。

test("toMatch", () => {
  expect("alice").toMatch("alice"); // 通过
  expect("alice").toMatch("lice"); // 通过
  expect("alice").toMatch("al"); // 通过
});
数组相关

toContain 用于判断数组是否包含指定元素,类似于 JavaScript 中的 includes 方法。

test("toContain", () => {
  expect(['banana', 'apple', 'orange']).toContain("apple");
  expect(['banana', 'apple', 'orange']).toContain("app"); // 不通过
});
error相关

toThrow 用于判断函数是否抛出异常,并可以指定抛出异常的具体内容。

test("toThrow", () => {
  const throwNewErrorFunc = () => {
    throw new TypeError("this is a new error");
  };
  expect(throwNewErrorFunc).toThrow();
  expect(throwNewErrorFunc).toThrow("new error");
  expect(throwNewErrorFunc).toThrow("TypeError"); // 不通过
});

以上就是各类型常用的匹配器。

命令行工具

package.json 中配置 script 指令,可以使 .test.js 文件在修改时实时自动执行测试用例。

"scripts": {
   "jest": "jest --watchAll"
},

在命令行中,你会实时看到当前测试用例的执行结果。同时,Jest 还提供了一些快捷配置,按下 w 键即可查看具体有哪些指令。

主要有以下几种类型:

f 模式

在所有测试用例中,只执行上一次失败的测试用例。即使其他测试用例的内容有修改,也不会被执行。

o 模式

只执行修改过的测试用例。这个功能需要配合 Git 来实现,根据本次相对于上次 Git 仓库的更改。这种模式还可以通过配置 script 指令来实现,即:

"script": {
"test": "jest --watch"
}

p模式

当使用 --watchAll 时,修改一个文件的代码后,所有的测试用例都会执行。进入 p 模式后,可以输入文件名 matchersFile,此时修改任何文件只会去查找包含 matchersFile 的文件并执行。

t模式

输入测试用例名称,匹配 test 函数的第一个参数。匹配成功后即执行该测试用例。

q模式

退出实时代码检测。

通过不同的指令,你可以更有针对性地检测测试用例。

钩子函数

在 Jest 中,describe 函数用于将一系列相关的测试用例(tests)组合在一起,形成一个描述性的测试块。它接受两个参数:第一个参数是一个字符串,用于描述测试块的主题;第二个参数是一个函数,包含一组测试用例。

即使没有显式定义 describe 函数,每个测试文件也会在最外层默认加上一层 describe 包裹。

在 describe 组成的每个块中,存在一些钩子函数,贯穿测试用例的整个过程。这些钩子函数主要用于测试用例执行之前的准备工作或之后的清理工作。

常用的钩子函数
  • beforeAll 函数在一个 describe 块开始之前执行一次
  • afterAll 函数在一个 describe 块结束之后执行一次
  • beforeEach 函数在每个测试用例之前执行
  • afterEach 在每个测试用例之后执行
示例代码

下面的示例代码展示了如何使用这些钩子函数:

describe("测试是否有值", () => {
  beforeAll(() => {
    console.log("beforeAll");
  });
  afterAll(() => {
    console.log("afterAll");
  });
  beforeEach(() => {
    console.log("beforeEach");
  });
  describe("toBeNull", () => {
    beforeAll(() => {
      console.log("toBeNull beforeAll");
    });
    afterAll(() => {
      console.log("toBeNull afterAll");
    });
    beforeEach(() => {
      console.log("toBeNull beforeEach");
    });
    test("toBeNull", () => {
      expect(null).toBeNull();
    });
  });
});
输出顺序

当运行上述测试用例时,输出的顺序如下:

beforeAll
toBeNull beforeAll
beforeEach
toBeNull beforeEach
toBeNull afterAll
afterAll

通过使用这些钩子函数,你可以更好地管理测试用例的生命周期,确保每次测试都从一个干净的状态开始,并在测试结束后清理掉产生的副作用。

在这一篇测试指南中,我们介绍了Jest 的背景、如何初始化项目、常用的匹配器语法、钩子函数。下一篇篇将继续深入探讨 Jest 的高级特性,包括 Mock 函数、异步请求的处理、Mock 请求的模拟、类的模拟以及定时器的模拟、snapshot 的使用。通过这些技术,我们将能够更高效地编写和维护测试用例,尤其是在处理复杂异步逻辑和外部依赖时。

全面掌握 Jest:从零开始的测试指南(上篇)的更多相关文章

  1. 《大话移动APP测试:Android与iOS应用测试指南》

    <大话移动app测试:android与ios应用测试指南> 基本信息 作者: 陈晔 出版社:清华大学出版社 ISBN:9787302368793 上架时间:2014-7-7 出版日期:20 ...

  2. 推荐——Monkey《大话 app 测试——Android、iOS 应用测试指南》

    <大话移动——Android与iOS应用测试指南> 京东可以预购啦!http://item.jd.com/11495028.html 当当网:http://product.dangdang ...

  3. OWASP固件安全性测试指南

    OWASP固件安全性测试指南 固件安全评估,英文名称 firmware security testing methodology 简称 FSTM.该指导方法主要是为了安全研究人员.软件开发人员.顾问. ...

  4. Web安全测试指南--文件系统

    上传: 编号 Web_FileSys_01 用例名称 上传功能测试 用例描述 测试上传功能是否对上传的文件类型做限制. 严重级别 高 前置条件 1.  目标web应用可访问,业务正常运行. 2.  目 ...

  5. Web安全测试指南--认证

    认证: 5.1.1.敏感数据传输: 编号 Web_Authen_01_01 用例名称 敏感数据传输保密性测试 用例描述 测试敏感数据是否通过加密通道进行传输以防止信息泄漏. 严重级别 高 前置条件 1 ...

  6. 读书笔记——商广明《Nmap渗透测试指南》

    一 Nmap基础学习 1.简介及安装 Nmap是一款由C语言编写的.开源免费的网络发现(Network Discovery)和安全审计(Security Auditing)工具.软件名字Nmap是Ne ...

  7. Mousejack测试指南

    0x00 前言 近日,Bastille的研究团队发现了一种针对蓝牙键盘鼠标的攻击,攻击者可以利用漏洞控制电脑操作,他们将此攻击命名为MouseJack. 攻击者仅需要在亚马逊上以60美元购买设备,改造 ...

  8. 测试指南(适用于Feature/promotion/bug)

    1.提前了解需求,在需求的业务基础和开发的架构基础上分析测试关键点,给出测试策略,甚至需要准备测试数据: 2.分析需求时不要受开发影响,要有自己的分析和判断,包括测试范围,测试时间: 3.在开始测试之 ...

  9. 微信小程序测试指南

    [本文出自天外归云的博客园] 微信小程序本地部署测试方法 下载微信开发者工具 让小程序管理员将测试人员的微信号添加开发者权限 本地设置hosts为测试环境hosts 打开微信web开发者工具并扫码登录 ...

  10. metasploit渗透测试指南概要整理

    一.名词解释 exploit 测试者利用它来攻击一个系统,程序,或服务,以获得开发者意料之外的结果.常见的 有内存溢出,网站程序漏洞利用,配置错误exploit. payload 我们想让被攻击系统执 ...

随机推荐

  1. docker卸载分享

    一.准备工作: 1.杀死docker有关的容器: docker kill $(docker ps -a -q) 2.删除所有docker容器: docker rm $(docker ps -a -q) ...

  2. Docker 容器数据:持久化

    Docker 容器数据:持久化 每当从镜像创建容器时,它都会创建一个新容器,除了镜像数据之外没有任何数据 意味着如果在提交更改之前删除容器,我们将丢失数据 Docker 应该存在一种将数据的文件系统与 ...

  3. JavaScript小面试~节流

    节流,当用户发出多次请求时,需要对事件进行限制,不要让事件过多触发.场景:在用户浏览页面的时候,用户拼命滚动屏幕时,控制页面滚动的事件会多次触发,会导致网络阻塞或者出现渲染差.此时需要对其进行约束.无 ...

  4. Tomcat 线程池学习总结

    前提 Tomcat 10.1.x Tomcat线程池介绍 Tomcat线程池,源于JAVA JDK自带线程池.由于JAVA JDK线程池策略,比较适合处理 CPU 密集型任务,但是对于 I/O 密集型 ...

  5. Python 阿里云OSS文件上传下载与文件删除及检索示例

    阿里云OSS文件上传下载与文件删除及检索示例 实践环境 运行环境: Python 3.5.4 CentOS Linux release 7.4.1708 (Core)/Win10 需要安装以下类库: ...

  6. app备案证明需要提供md5值和公钥的解决方案

    现在app上架华为市场.小米市场.苹果市场等大型的应用商店,都需要提供国内的app备案证明.无论是安卓还是ios,都需要备案了. 但是问题是备案的时候需要填写app的bundle ID.公钥和MD5值 ...

  7. Jmeter函数助手17-StringtoFile

    StringtoFile函数用于将字符串写入文件 Path to file (absolute):将写入的文件路径 String to write:要写入的字符 Append to file (tru ...

  8. 从.net开发做到云原生运维(一)——从.net framework过渡到.net core

    1. 前言 序篇讲了自己的一些感悟和经历,从这章开始就开始讲一些.net技术栈的东西了. 2. .net framework和.net core对比 .NET Framework 概述 .NET Fr ...

  9. 寻路数据集 —— PathFinding数据集 —— Moving AI Lab. 实验室

    好几个做pathfinding的论文都是引用这个网站的数据集,不过这个网站的数据集的地图都是 .map 格式,这个类型的格式该如何打开还不知道. Moving AI Lab. 实验室的工作 地址: h ...

  10. Ax = b 的迭代解法 —— 共轭梯度 (算法步骤)

    线性方程组 Ax =b 除了高斯消元法以外,还有其它的迭代解法,这里我们说的是共轭梯度法. 这里只针对 A 满足 对称 (  ), 正定(即  ),并且是实系数的,那么我们可以用 梯度下降 和 共轭梯 ...