# 测试用例 mocha,should,istanbul
30 秒速记
- 三件套分工:
mocha跑用例,should写断言,istanbul统计覆盖率 - 现在的选择:
istanbul命令行已停更,用nyc或c8;Node 20起也有内置的node --test和覆盖率 - 测试要稳定可重复:时间、网络、数据库、随机数都隔离或
mock掉 - 覆盖率只说明「代码跑过」,不说明「结果对」,重点盯分支、异常和边界
- 异步用例
return一个Promise或用async函数,别和done混用
这三个工具各管一件事:mocha 负责组织和运行测试,should 负责写「结果应该是什么」的断言,istanbul 负责统计哪些代码被测到了。 这套组合比较老,现在我会这么选:运行器还是 mocha 或者换 vitest,断言用 Node 自带的 assert 或 chai,覆盖率用 nyc 或 c8,istanbul 的命令行早就不维护了,nyc 就是它的继任者。不管用哪套,思路是一样的:用例要覆盖正常值、边界和异常,覆盖率报告用来找漏测的分支,而不是追求一个好看的百分比。
// test/sum.test.js,运行:npx mocha
const assert = require('node:assert')
const sum = (a, b) => a + b
describe('sum', () => {
it('1 + 2 = 3', () => assert.strictEqual(sum(1, 2), 3))
it('async 用例直接 return / await', async () => {
const v = await Promise.resolve(sum(2, 2))
assert.strictEqual(v, 4)
})
})
理解「测试用例 mocha,should,istanbul」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 testing 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
mocha + should + istanbul 是早期 Node 项目的经典测试组合,三者分工明确。
mocha 是测试运行器,提供 describe / it 组织用例,before / after / beforeEach / afterEach 做准备和清理,负责收集结果和退出码。它本身不带断言。
should 是断言库,它在 Object.prototype 上挂了一个 should 属性,所以能写成 (5).should.above(3)。缺点是 null 和 undefined 上没有这个属性,要改用 should(x).be.null()。同类的还有 chai(expect / should / assert 三种风格都有)和 Node 内置的 node:assert。
istanbul 是覆盖率工具,会统计四个维度:语句、分支、函数、行。它的命令行 istanbul cover 已经停止维护,现在用的是 nyc(同一个团队维护,基于 istanbul 的插桩库)或者 c8(直接用 V8 引擎内置的覆盖率数据,不用插桩)。
按今天的习惯,一个最小的配置可以这样写:
{
"scripts": {
"test": "mocha",
"cov": "c8 mocha"
},
"devDependencies": { "mocha": "^10.0.0", "c8": "^10.0.0" }
}
npm run 会自动把 node_modules/.bin 加进 PATH,所以脚本里直接写 mocha 用的就是项目本地安装的版本,不需要全局安装,也不需要 Makefile。Node 20 以后还可以用零依赖的内置测试:node --test 加 --experimental-test-coverage 就能出覆盖率。
面试官追问
追问 1mocha 全绿,覆盖率 100%,能说明代码没问题吗?
不能。覆盖率只统计代码有没有被执行,一个没有断言的用例也能把覆盖率刷满。要看用例里有没有真正断言返回值和异常。
追问 2异步用例忘了 return,会怎样?
mocha 认为函数执行完就通过了,Promise 里的断言失败根本没人接,用例照样绿。用 async 函数加 await,或者显式 return 那个 Promise。
追问 3同一个用例里既接了 done 参数又返回了 Promise,会怎样?
新版 mocha 会直接报错 Resolution method is overspecified。两种方式选一种,现在基本都用 async/await,done 只留给纯回调风格的老代码。
追问 4测试本地通过,CI 上偶尔失败,最常见的原因是什么?
用例之间共享了状态,或者依赖了真实时间、网络、随机数,执行顺序和速度一变就挂。时间用 sinon 的假时钟,网络用 nock 拦截,每个用例在 beforeEach 里重置状态。
# 《测试用例:mocha,should,istanbul》
# 目标
30 秒速记
- 需求:
fibonacci(0) = 0,fibonacci(1) = 1,fibonacci(n) = f(n-1) + f(n-2),fibonacci(10) = 55 - 三种非法输入要抛错:
n > 10、n < 0、n不是数字 - 测试目标:
test/main.test.js里行覆盖率和分支覆盖率都要100% - 用例清单 =
0、1、10三个正常值 +11、-1、'abc'三个异常值 - 易漏:
NaN的typeof也是number,小数也要想清楚接不接受
这个练习的目标是先把函数的规则写清楚,再用测试把每条规则都钉住,最后让行和分支覆盖率都到 100%。 fibonacci 的规则有六条:0 返回 0,1 返回 1,大于 1 走递归,再加上大于 10、小于 0、不是数字三种抛错。所以测试至少要六个用例,一条规则一个,光测一个 fibonacci(10) === 55 是远远不够的。我写这类测试的习惯是先把规则列成清单,每条对应一个 it,用例名直接写规则本身,以后哪条挂了一眼就知道是哪个需求被破坏了。
建立一个 lesson6 项目,在其中编写代码。
main.js: 其中有个 fibonacci 函数。fibonacci 的介绍见:http://en.wikipedia.org/wiki/Fibonacci_number 。
此函数的定义为 int fibonacci(int n)
- 当 n === 0 时,返回 0;n === 1时,返回 1;
- n > 1 时,返回
fibonacci(n) === fibonacci(n-1) + fibonacci(n-2),如fibonacci(10) === 55; - n 不可大于10,否则抛错,因为 Node.js 的计算性能没那么强。
- n 也不可小于 0,否则抛错,因为没意义。
- n 不为数字时,抛错。
test/main.test.js: 对 main 函数进行测试,并使行覆盖率和分支覆盖率都达到 100%。
面试官追问
追问 1只断言了 fibonacci(10) === 55,覆盖率能到多少?
分支覆盖率到不了 100%,三条抛错分支一次都没走。0、1 虽然会在递归里被执行到,但没有单独断言它们的返回值,基线写错了也不一定能第一时间发现。
追问 2传 NaN 进去会怎样?现有的 typeof n !== 'number' 拦得住吗?
拦不住,typeof NaN 是 'number'。NaN 和什么比较都是 false,三个判断全部跳过,一直递归到 RangeError: Maximum call stack size exceeded。改成 if (!Number.isInteger(n)),NaN、小数、Infinity 一起挡掉。
追问 3上限从 10 改成可配置,测试怎么写才不会每次改配置都要改用例?
用例围绕边界写:上限值本身能算、上限加一抛错,上限从配置里读。负数、非数字、基线这些和上限无关的用例保持不动。
追问 4上限放宽到 50,递归实现还能用吗?
不能,朴素递归是指数级的,n = 50 要调用几百亿次。改成迭代或者加缓存,O(n) 就算完了;改实现时原来的测试正好用来回归。
# 知识点
30 秒速记
- 测试框架
mocha:describe分组、it写用例、before/after钩子,负责跑和报告结果 - 断言库
should:(5).should.above(3)这种链式写法,可以换成chai或node:assert - 覆盖率工具
istanbul:统计语句、分支、函数、行四个维度,现在用nyc或c8 Makefile:把长命令收成make test、make cov;现在更常用package.json的scripts
这节要掌握四样东西:mocha 跑测试、should 写断言、istanbul 看覆盖率、Makefile 把命令收起来。
