# 单测(Unit Test)技巧

# 前言

本文目的是提高编写单测的效率,适合于有一定单测编写经验,但被单测困扰的同学。
后文的示例都在 unit-test-examples (opens new window) 仓库中。

# 单测的意义与价值

单测本质:将测试行为及结果固化下来,自动检查被测试代码的运行结果是否符合期望。

  • 单测是一种调试工具;在开发阶段验证代码是否符合期望,比浏览器中调试更有效率
  • 单测是一种项目文档;帮助了解SDK的API及如何使用
  • 单测能降低项目维护成本;拥有完整的单测用例后,单测执行结果会告诉你代码变更的影响范围
  • 单测能帮助你编写更加优秀的代码

# 编码技巧

# 单测的边界

单元测试的目标是你写的代码,测试你写的代码符合期望(白盒测试),边界之外代码若有意外尽量 Mock。
如何确定边界?

  1. 导入的第三方包
  2. 系统(Node.js, 浏览器)提供的部分 API,主要包括 fetch、document、fs

使用Mock的方法技巧参考后文。

# 编写易于测试的代码

优秀的代码有很多要素,优秀的代码肯定是易于测试的。

# 隔离副作用代码

函数副作用是指函数在正常工作任务之外对外部环境所施加的影响。

对JS来说,最常见的副作用是 网络请求、增删DOM节点、读写文件;
副作用不可避免,但可以隔离后只测试相对比较纯的函数。

展开查看示例代码
// 错误示例
function createList(arr) {
  const ulEl = document.createElement('ul')
  arr.forEach((it) => {
    const li = document.createElement('li')
    li.textContent = it
    ulEl.appendChild(li)
  })
  // 其他逻辑。。。
  document.querySelector('container').appendChild(ulEl)
}

// 正确示例;检测返回值 比 mock document要简单得多
function createList(arr) {
  const ulEl = document.createElement('ul')
  arr.forEach((it) => {
    const li = document.createElement('li')
    li.textContent = it
    ulEl.appendChild(li)
  })
  // 其他逻辑。。。
  return ulEl
}

// 对于汇总了副作用的函数,可以使用 Mock 方法进行测试
// 也可以考虑略过,不编写测试代码,权衡成本即可
function insertList() {
  const arr = [1, 2]
  document.querySelector('container').appendChild(createList(arr))
}

# 控制圈复杂度

圈复杂度 (opens new window)简单来说就是逻辑分支越多,圈复杂度越高,单测用例覆盖代码越困难。

圈复杂度跟业务复杂度相关,无法完全避免,但可通过编码技巧降低或拆分成多个函数,降低单测难度。

展开查看示例代码
// 错误示例
function envStr2Code(env) {
  if (env === 'dev') {
    return 1
  } else if (env === 'test') {
    return 2
  } else if (env === 'prod') {
    return 3
  } else {
    return -1
  }
}

// 如果要覆盖 所有语句
expect(envStr2Code('dev')).toBe(1)
expect(envStr2Code('test')).toBe(2)
expect(envStr2Code('prod')).toBe(3)
expect(envStr2Code('unknown')).toBe(-1)

// 正确示例
function envStr2Code(env) {
  return ({ dev: 1, test: 2, prod: 3 })[env] ?? -1
}
// 类似的还有
function envCode2Str(code) {
  return [null, 'dev', 'test', 'prod'][code] ?? 'unknown'
}

参考: 圈复杂度优化 (opens new window)

圈复杂度常用优化方法

  1. 算法优化
  2. 表达式逻辑优化
  3. 大函数拆小函数

# Jest 技巧

Jest是当前最流行的JS单测框架,下文介绍单测高频使用的技巧来提高编写单测代码的效率,未入门请阅读官方文档 (opens new window)

# Expect(断言) (opens new window)

基础的值判断 .toBe.toEqual 等就不介绍了

  • 当检测的数据只知道类型,但具体值是不确定的,就使用expect.any(constructor)
  • 当只需要检测数据的特征,如字符串的子串、数组的值、对象的key,使用expect.stringContainingexpect.arrayContainingexpect.objectContaining
  • toHaveBeenCalled用于判断Mock函数(通常是jest.fn)是否执行,还有多个以toHaveBeenCalled开头的函数用于判断执行Mock函数的参数、次数

expect 示例 (opens new window)

# Mock

# Timer

JS是单线程异步执行代码,所以需要API能精确控制定时器回调函数的执行时机,来完全掌控被测试代码的执行。

  • jest.useFakeTimers()所有timer 定时器 (opens new window)都会停止运行,需手动控制来执行定时器的回调函数
    • jest.advanceTimersByTime(msToRun)相当于时间往前拨N毫秒,满足的条件的定时器回调函数将被执行
    • jest.advanceTimersToNextTimer(steps)相当于时间往前拨一定时间(不确定),恰好让第1..N个定时器回调被执行,是jest.advanceTimersByTime的快捷方式,控制次数而不是时间,参考解释 (opens new window)
  • jest.useRealTimers()恢复真实定时器,jest.useFakeTimers()的反操作
  • jest.runAllTicks()执行所有微任务队列
  • jest.runAllTimers()执行所有宏任务队列

示例代码 (opens new window)

# Snapshot 快照 (opens new window)

快照经常用来检测 UI(DOM)结构是否符合期望,实际上只要检测的数据比较复杂(比如一个复杂JSON)就可使用快照来简化测试代码。

快照是把上次检测的值序列化为字符串保存到本地文件中,后续检测如果不一致,单测用例就会报错。

若结果变化符合期望,则需在交互界面按下【u】键去更新本地文件中的快照内容。

如果快照中包含随机数、时间戳、id之类每次都会变化的值,默认情况每次执行结果都与上次有差异,导致用例失败,可参考官方示例 (opens new window)

复杂数据快照示例 (opens new window)

# 配合 vscode

配合 vscode,在保存代码实时运行单测用例,反馈执行结果;且能在编辑器中随时断点 Debug。

参考 vscode 配置:.vscode/launch.json (opens new window)

# Watch 模式

jest 启用 Watch 模式,会监听文件变化自动执行单测用例。

--watch (opens new window)表示vscode中运行单测时启用 Watch 模式。

Debug 代码时,经常需要执行特定的用例,避免干扰。

介绍几个Jest交互模式下高频使用的快捷键:

  • f: 仅运行失败的单测用例
  • p: 仅执行测试文件名匹配的单测用例
  • t: 仅执行测试用例名(test('用例名'))匹配的用例
  • u: 更新快照文件(前文介绍了)
  • q: 退出
💗 博主正处于裸辞待业状态,欢迎 商务合作 💗

相关文章

从 React 看前端 UI 代码范式革命

alt text 前言 本来打算写的主题是“我为什么讨厌 React Hooks API”,展开聊聊“小甜甜”是如何变成“牛夫人”的,没想到越写越严肃:) React 是两次前端范式革命的引领者,至今仍有繁荣的社区和旺盛的创造力; React 多次天才又激进的创新,一些想法被借鉴改良、一些引发广泛质疑,大部分是被认同和接受的; ...

一句话总结:TS 何时选择 interface 或 type

用 interface 描述类型的结构,用 type 描述类型关系。 有点编程基础中数据结构与算法的味道。 结构即是类型的属性集合 // 如 Point3D 的属性集合: x, y, z。 interface Poi ...

JS 定时器时长控制细节

背景 JS 最常使用 setTimeout、setInterval 来延迟或定时循环执行函数,通常会传递第二个参数来控制延迟或间隔执行的时间。 但开发者必须意识到函数执行时间并非精确地符合预期,在以下场景中它会超出你的预期 CPU 繁忙(主线程被长时间占用),JS 无法按开发者设定的预期时间延迟函数 定时器过于频繁地执行(第二个参数 < 4),达到一定 ...

JS 多线程并发

[[toc]] 为什么需要并发 我们常听说 JS 是单线程模型,即所有代码都在主线程中执行的。 如果某些任务计算量较大,将阻塞主线程,UI 界面轻则掉帧、重则卡死。 // 提示:本文所有代理均可复制到浏览器控制台中执行,验证效果 // ...

系统化学习 TS 类型系统

目的:快速、系统性的入门 TS 类型系统 [[toc]] 前言 TS 是什么? TS 是 JS 的超集, TS = JS + 类型系统 为了描述如此复杂(由于 JS 语言的灵活性/复杂性)的类型信息,类型系统表现出非常明显的编程语言特性。 以学习编程语言的方式,来学习 TS 类型系统 关键字/符号 类型: boolean, number, stri ...

跨域(Options)请求介绍及解决方法

介绍 OPTIONS请求指method为OPTIONS的http请求。 通俗来说:它的作用是用于WEB服务器是否支持某些 header,也可以叫做预检请求(顾名思义:预先检测)。 程序员:跨域发送 http get { headers { xxx: abc } } 浏览器:等等,你这个请求有点奇怪,我去跟服务器确认下 浏览器:发送 http options ...

compilerOptions字段详解

{ "compilerOptions": { /* 基本选项 */ "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' "module": ...

JS 正则表达式基础

前言 个人经验,正则是一个前期少量投入,回报超高的技能点。 其适用范围非常广泛,如批量文本处理、源码替换、程序中逻辑判断等等。 本文只介绍常用的基础知识、技巧,让初学者快速掌握大部分日常所需的正则知识。目标是 5 分钟内可逐字读完,10 分钟内可把例子都动手实践一 ...

JS优缺点

回顾上两期 优点(简单) 对象 链(原型链 & 作用域链) 一切都是对象(包括函数),构建世界的原料,越少越简单、灵活。 jimu 观察者模式,例: class Observer { constructor() { this.subscribe ...

JS 数据处理

[[toc]] 分享目标 JS 数据处理技巧速成 让你感叹:JS 还可以这样写 基础 实践原则 数据处理时,尽量避免创建临时变量(特别是 let)、修改参数、改变外部引用、for、if 等 分离 数据处理 与 副作用(DOM 操作、存储、网络请求等)代码 数据处理指:数值计算和数据结构变换。 第一点:如何避免? **1. 熟 ...