Hey guys! 最近找工作面试的时候,有位面试官问到单元测试,真是问到一个我完全未知的领域。今天就来仔细地扒一扒什么是单元测试。
概念和使用场景
单元测试是指软件中最小可测试单元进行检查验证, 也称为模块测试。在node.js 中通过是对某个函数, 模块 , API进行正确验证, 以保证代码的可用性。
使用场景
- 分支条件很多,边界条件很多
- 组件库之类的核心/关键模块
- 工具函数,API之流不依赖外部环境的函数
- CI/CD的一环质量红线
BDD和TDD
BDD vs TDD(区别)
| 对比项 | TDD(测试驱动开发) | BDD(行为驱动开发) |
|---|---|---|
| 关注点 | 功能是否正确实现 | 行为是否符合用户期望 |
| 测试描述方式 | 技术导向(代码逻辑) | 自然语言(行为描述) |
| 测试命名 | testAddFunction() | should add two numbers correctly |
| 编写者 | 开发者 | 开发者 + QA + 产品 |
| 常见框架 | JUnit, unittest | Mocha, Jasmine, Jest, Cucumber |
TDD 风格
1 | const assert = require('assert'); |
BDD 风格
1 | import { expect } from 'chai'; |
BDD 的好处有:
- 测试代码更接近自然语言,非技术人员也能理解
- 更容易维护与文档同步(每个
describe/it都像行为文档) - 鼓励先写“行为”,再实现功能
- 结合工具(如 Cucumber)可以直接从“用户故事”自动生成测试
断言(Assert)的概念
assert 模块提供了断言测试的函数,用于测试不变式。它是Node.js里自带的核心模块。有如下的一些常用方法:
assert.strictEqual(actual, expected[, message])判断全等 (===)
1 | const assert = require('assert'); |
assert.notStrictEqual(actual, expected[, message])判断不全等 (!==)
1 | assert.notStrictEqual(2 + 2, '4'); // ✅ |
assert.deepStrictEqual(actual, expected[, message])深度比较两个对象/数组的内容(要求类型也相同)。
1 | assert.deepStrictEqual({a: 1}, {a: 1}); // ✅ |
-
assert.ok(value[, message])判断值是否为 真值。 -
assert.fail([message])直接让测试失败。
覆盖率(Coverage)
覆盖率(Coverage) 是衡量测试用例“覆盖了多少代码”的指标。如我们需要检查分支是否被单元测试全部涉及,所有函数是否被涉及……
还是以刚刚的math模块方法为例,假设我们增加一个绝对值方法abs():
1 | // math.js |
为了检测单元测试代码是否覆盖了所有,我们需要在测试脚本里加上依赖或者参数配置。比如mocha框架是需要安装nyc依赖,而jest框架则自带覆盖率统计。
以jest为例,在package.json中加上测试命令:
1 | { |
效果如:

单元测试工具
测试用例的基本语句
node.js测试用例有4个基本语句:
describe: 定义一个测试套件it: 定义一个测试用例expect:断言的判断条件toEqual: 断言的比较结果
测试框架
mocha
特点:
- 功能非常丰富, 支持 BDD, TDD
- 支持运行在 node.js 和浏览器中
- 对异步测试支持非常友好
- 支持 4种hook, 包括 before/after/beforeEach/afterEach
使用mocha,你需要事先安装mocha:
1 | npm i mocha |
然后在你的package.json里配置脚本执行mocha:
1 | "scripts": { |
如此,之后在命令行里可以通过npm test来执行你的单元测试脚本,它会默认在项目中寻找 test/ 目录或 *.test.js 文件,然后执行其中用 describe、it 等语法定义的测试。

jest
相比mocha,jest内容更加全面,集成了断言,jsdom模拟浏览器DOM环境。
动手写一个单元测试
函数测试
首先我们先熟悉一下测试部分的几个核心组成部分:
- 顶层 describe()
1 | describe('MyFunction 单元测试', () => { ... }) |
describe 用于给一组相关的测试命名。一个文件里可以有多个 describe,用来分模块组织测试。
- 测试用例 it/test
1 | it('确认函数执行结果正确', () => { ... }) |
it(或 test)表示一个独立的测试用例。it彼此之间应当是高内聚低耦合的。
1 | it('should return the correct sum', () => { |
这里贴一个可以跑通看效果的case:
math.js:
1 | export function add(a, b) { |
test/math.test.js:
1 | import { add, minus } from "../math.js"; |
组件测试
测试文件代码结构
常见的测试文件的结构:
1 | // ① 导入依赖 |
组件测试的it执行用例会比函数测试更复杂一些:
1 | it('xxx 功能描述', async () => { |
这里需要记住几个较为核心的生命周期钩子用法:
| 钩子 | 执行时机 | 用途 |
|---|---|---|
beforeAll |
所有测试前执行一次 | 设置全局 mock、全局配置 |
beforeEach |
每个 it 前执行 |
初始化 wrapper、重置数据 |
afterEach |
每个 it 后执行 |
销毁 wrapper、清理定时器 |
afterAll |
所有测试结束后执行一次 | 清理全局资源 |
以一个弹窗组件的单元测试为例
举一个例子,我们对一个elementUI风格的modal(弹窗)组件进行单元测试。假设我们有一个这样的modal组件:
1 | <!-- Modal.vue --> |
我们使用jest测试框架,对这个vue组件进行测试,我们可以列出一些组件的测试目标:
| 测试点 | 要验证的内容 |
|---|---|
| 1. 默认不显示 | visible=false 时,DOM 中不应有 .modal |
| 2. 显示弹窗 | 传入 visible=true 时,出现 .modal |
| 3. 点击关闭按钮 | 点击 .close-btn 时,触发 close 事件 |
| 4. 支持 slot 内容 | 传入 slot 内容应正确渲染 |
| 5. 清理挂载节点 | 每次测试后清理 DOM(afterEach) |
然后再将上述思路整理为测试文件:
1 | // Modal.test.js |
配置CI/CD流水线自动执行
为了能够更好的阐释,我会以本博客的部署Github Action为例来实践单测的流水线接入,且听下回分解。
codesandbox尝试
为了实践一下,建了一个nodejs的沙箱环境,安装如下依赖:
1 | npm install vue@2 @vue/test-utils jest vue-jest babel-jest babel-core@^7.0.0-bridge.0 --save-dev |
sandbox项目结构如下:
1 | sandbox/ |
我这里使用的是vue2的框架做组件测试,jest测试框架是在Node环境下运行测试,而Vue组件需要依赖浏览器环境语法,所以需要一些转译方法将它们转成node可执行的JS,尤其需要注意的是CommonJS与ESM语法的不同,不要混用两者。
- CommonJS(CJS) →
require()、module.exports- ES Module(ESM) →
import ... from、export default- Node.js 默认识别
.cjs为 CommonJS,.mjs或"type":"module"为 ESM。
这里还有一个可能遇到的问题就是:
Jest encountered an unexpected token
Unexpected token ‘export’
这是一个可能遇到的转译问题,一个解决方案是使用Babel,来理解ES6的导入导出语法。
babel.config.js:
1 | module.exports = { |
比较菜的前端同学(比如我),可能不是特别清楚babel到底是啥,补一个好记的理解:
Babel 是一个 JavaScript 编译器,用来把新版本的 JS 语法转换成旧版本浏览器或 Node.js 能理解的代码。
jest.config.js:
1 | module.exports = { |
完成配置之后,可以用npm test执行测试:
执行单元测试成功效果如下:

附上沙箱的链接,里面会有可用的依赖配置:
unit test📦
结语
以上阐释均为入门级别的测试框架使用和单元测试概念了解,煮啵写这篇博客的时候翻阅资料,了解了许多测试框架的用途,实际使用的时候还是需要结合文档多尝试,在实验中去完善认知。