Node.js 单元测试与性能测试指南
写给普通开发者的测试入门手册,看完终于不怕写测试了
那天凌晨三点,手机响了。
你从被窝里伸出手,迷迷糊糊摸到屏幕上的红色按钮——OnCall 告警。线上崩了。你盯着 Grafana 上的红色折线,CPU 打满,内存泄漏,错误率飙升到 40%。你心里想的第一句话是什么?卧槽,这个接口我昨天才改过,本地明明跑得好好的。
你翻日志,翻 commit 记录,翻那行代码。你发现一个同事在合并他的 feature 时改了一个共享工具函数的参数校验逻辑,你的模块挂了。没人发现。因为没人写测试。
你骂了一句,开始改。改完,上线,重启,告警消失。你躺在床上,睡不着。你问自己:这种事会不会还有第二次?
如果你想让这种事不再发生,这篇文章是写给你的。我不是来教你那些高大上的测试理论,我是来告诉你:作为一个普通程序员,你怎么用测试让你的代码活得久一点,让你睡得安稳一点。
先讲清楚一件事:测试到底解决什么问题
很多人对测试的理解是歪的。
有人觉得测试就是"证明我的代码是对的"——错了,测试是来告诉你"你的代码可能在什么地方错"。有人觉得测试就是"写完代码走个过场"——也错了,测试是写给你六个月后的自己的。有人觉得测试就是"追求 100% 覆盖率"——更扯了,100% 覆盖率不等于代码没 bug。
测试真正解决的是信任问题。
你改了一行代码,你敢不敢点 "deploy"?你自己心里清楚——如果每次上线前你都得深吸一口气,在心里默念"应该没事吧",那你他妈的就是在赌。
别赌。写测试。
具体来说,测试帮你做到这几件事:
防退化。改了 A,B 挂了。这叫回归。自动化测试能在合并之前就告诉你。
省时间。一个 assert 失败信息,比你满屏的 console.log(response, 'response ===>') 快多少?你想想你上个月花了多少时间在 console 里找 bug。
敢重构。你盯着那坨 500 行的函数看了三个月了,你知道它能优化,但你不敢动。因为你不知道改动会影响到哪里。有了测试,你就有了安全网。
当文档。代码注释会过期,但测试不会——测试跑不过就会报错,它逼着人维护。看测试代码比看注释靠谱得多。
想象一下你装修房子。工人把水管电线走暗线,封上墙之前你得做验收吧?你不会等墙都抹好了、漆都刷了,才来检查水管是不是漏水。单元测试就是这个验收——在你还看得见的时候,确定每个部件是对的。
第一部分:单元测试——你代码的底裤
什么叫单元测试?
就是对你代码里最小可用的那块东西单独做检查。
一般是啥?一个函数。一个方法。一个小模块。
关键词是"单独"。如果你的测试需要连数据库、调外部 API、读 Redis,那它已经不算单元测试了——那叫集成测试,是另一回事。
单元测试就像你买了一台冰箱,你插上电看看它制冷不制冷。你不会把冰箱搬到客户家里再试——那叫验收测试,中间的过程叫愚蠢。
为什么得写?我给你算个账
你可能觉得"我这个函数逻辑很简单,不用测",或者"测试太费时间,直接上线吧"。
来,看这张表:
| 阶段 | 发现 bug 的成本 |
|---|---|
| 编码时发现 | 1x |
| 单元测试发现 | 1x - 10x |
| 集成测试发现 | 10x - 50x |
| QA 测试发现 | 50x - 200x |
| 生产环境发现 | 200x - 1000x |
生产环境发现一个 bug,成本是编码时的 200 倍。而且成本不只是钱——是你的时间,你的心情,你半夜被叫醒的那个电话。
单元测试的真正价值是反馈速度。代码刚写完,你脑子里逻辑还热乎着,这时候发现 bug,两分钟改完。等一个月后再发现——你连当时为什么那么写都忘了。
怎么写?AAA 模式
没那么多花里胡哨的,就三块:
- Arrange(准备):把你要测的东西搭好。数据、mock、依赖。
- Act(执行):调用那个函数。
- Assert(断言):看结果对不对。不对就报警。
这就是一个三明治——准备是面包,执行是肉,断言是另一片面包。缺一块就不能叫三明治。
上手试试
假设你写了个计算器模块:
// math.js
export function add(a, b) {
return a + b
}
export function subtract(a, b) {
return a - b
}
export function multiply(a, b) {
return a * b
}
export function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为零')
}
return a / b
}
下面是测试代码。注意看 AAA 结构:
// math.test.js
import assert from 'node:assert'
import { add, subtract, multiply, divide } from '../math.js'
describe('计算器模块测试', () => {
describe('add 函数', () => {
it('应返回两个正数的和', () => {
// Arrange
const a = 2
const b = 3
const expected = 5
// Act
const result = add(a, b)
// Assert
assert.strictEqual(result, expected)
})
it('应正确处理负数', () => {
assert.strictEqual(add(-1, 1), 0)
assert.strictEqual(add(-5, -3), -8)
})
it('应正确处理零', () => {
assert.strictEqual(add(0, 0), 0)
assert.strictEqual(add(5, 0), 5)
})
it('应正确处理小数', () => {
assert.strictEqual(add(0.1, 0.2), 0.3)
})
})
describe('divide 函数', () => {
it('除数为零时应抛出错误', () => {
assert.throws(() => divide(10, 0), /除数不能为零/)
})
})
})
运行结果:
计算器模块测试
add 函数
✓ 应返回两个正数的和
✓ 应正确处理负数
✓ 应正确处理零
✓ 应正确处理小数
divide 函数
✓ 除数为零时应抛出错误
5 passing (15ms)
15 毫秒,5 个测试全过。这就是你以后每次改代码都能跑一遍的东西。
注意:除数是零这种情况,是你写代码时最容易漏掉的。测试逼着你把边界条件想清楚。这就是为什么说"写测试帮你设计更好的代码"——不是玄学,是你真的会多想想。
什么样的测试算好?FIRST 原则
不是写了就有用。坏的测试比不写还糟——它给你虚假的安全感。
记住 FIRST:
Fast(快)。单元测试要快。你一个项目几百个测试用例,如果每个跑几秒,你就不想跑了。你不跑,就白写了。
Independent(独立)。每个测试各管各的,别他妈的串一起。A 挂了不影响 B 跑。测试之间不能有依赖顺序。
Repeatable(可重复)。跑一百次结果都一样。跟时间相关的东西要 mock 掉,跟网络相关的要 mock 掉。你不能因为今天是 5 月 1 号就测试失败。
Self-Validating(自验证)。结果必须明确——过了就是过了,挂了就是挂了。不要搞什么"输出一个报告让 leader 确认"。
Timely(及时)。写完代码立刻写测试,别拖。拖了一周你再写,你连那个函数叫什么名字都要去查 git blame。
覆盖率:80% 就够了,不用纠结 100%
覆盖率衡量的是你的测试到底跑了多少代码。四个常用指标:
| 指标 | 含义 | 例子 |
|---|---|---|
| 语句覆盖 | 每行代码是否执行过 | if (x) { doSomething(); } —— 如果 x 永远是 false,这行就不算覆盖 |
| 分支覆盖 | 每个 if-else 分支都走了吗 | 拿到 true 和 false 两种结果才算 |
| 函数覆盖 | 每个函数都调用过吗 | 别留孤岛函数 |
| 行覆盖 | 每行都执行过 | 和语句覆盖差不多 |
覆盖率报告长这样:
File | % Stmts | % Branch | % Funcs | % Lines
math.js | 85.71 | 75 | 100 | 85.71
user.js | 90.00 | 80 | 100 | 90.00
math.js 的语句覆盖率 85.71%,分支覆盖率 75%。什么意思?有 25% 的分支没测到,可能藏着雷。
但覆盖率是不是越高越好?
不是。 覆盖率数字是副产品,不是目标。
我见过为了 100% 覆盖率写的测试——全是 expect(1 + 1).toBe(2) 这种狗屁。数字好看了,但你核心逻辑的边界条件一个没测。这叫自欺欺人。
我的建议:
- 核心业务逻辑追 80% 以上
- 边界条件、异常处理,必须测
- 简单 getter/setter、模板代码,不用测
- 不要为了凑数写没意义的 assert
Node.js 测试框架:三家分晋
测试框架选哪个?市面上三巨头:Mocha、Jest、Vitest。给你一个快速指南:
| 特性 | Mocha | Jest | Vitest |
|---|---|---|---|
| 断言库 | 需要自己装(Chai) | 内置 | 内置 |
| Mock/Stub | 需要 Sinon | 内置 | 内置 |
| 并行执行 | 需要配置 | 自动 | 自动(基于 Vite,极快) |
| ESM 支持 | 需要配置 | 基本支持 | 原生支持 |
| TypeScript | 需要配置 | 内置 | 内置 |
| 适合谁 | 老项目、爱折腾的 | React 项目 | Vue/Vite 项目、新项目 |
简单做个判断:新项目直接 Vitest,React 项目 Jest,老项目或者框架自定义需求高的用 Mocha。
下面分别看实战。
Mocha + Chai + Sinon:老派组合,灵活但折腾
Mocha 本身只是个测试运行器。断言、Mock 都要自己搭。适合那种"我就想自己控制每一个环节"的团队。
测试一个 API 调用函数:
// api.js
import axios from 'axios'
export async function fetchUser(userId) {
const response = await axios.get(`/api/users/${userId}`)
return response.data
}
// api.test.js
import { expect } from 'chai'
import sinon from 'sinon'
import axios from 'axios'
import { fetchUser } from './api.js'
describe('fetchUser', () => {
let stub
beforeEach(() => {
stub = sinon.stub(axios, 'get')
})
afterEach(() => {
stub.restore()
})
it('应成功获取用户数据', async () => {
const mockUser = { id: 1, name: '张三' }
stub.resolves({ data: mockUser })
const result = await fetchUser(1)
expect(result).to.deep.equal(mockUser)
expect(stub.calledOnce).to.be.true
expect(stub.calledWith('/api/users/1')).to.be.true
})
it('用户不存在时应抛出错误', async () => {
const error = new Error('Request failed with status 404')
error.response = { status: 404 }
stub.rejects(error)
await expect(fetchUser(999)).to.eventually.be.rejected
})
})
Jest:开箱即用,React 亲儿子
Jest 最大的好处是零配置。断言有了,mock 有了,覆盖率有了,watch 模式也有了。Faceboook 出的,跟 React 绑得最紧。
// user.js
export function createUser(name, email) {
if (!name || !email) {
throw new Error('姓名和邮箱不能为空')
}
return {
id: Date.now(),
name,
email,
createdAt: new Date().toISOString(),
}
}
// user.test.js
import { createUser } from './user.js'
describe('createUser', () => {
test('应返回包含 id、name、email 的对象', () => {
const user = createUser('张三', 'zhangsan@example.com')
expect(user).toHaveProperty('id')
expect(user).toHaveProperty('name', '张三')
expect(user).toHaveProperty('email', 'zhangsan@example.com')
expect(user).toHaveProperty('createdAt')
})
test('空姓名应抛出错误', () => {
expect(() => createUser('', 'test@example.com')).toThrow('姓名和邮箱不能为空')
})
test('空邮箱应抛出错误', () => {
expect(() => createUser('张三', '')).toThrow('姓名和邮箱不能为空')
})
})
Vitest:新一代,快得离谱
Vitest 兼容 Jest 的 API 但底层跑在 Vite 上。启动速度、热更新、watch 模式的体验都甩 Jest 一截。如果你用 Vue 或者 Vite 做构建工具,没理由不用。
// arrayUtils.js
export function filterByStatus(items, status) {
return items.filter((item) => item.status === status)
}
export function groupByCategory(items) {
return items.reduce((acc, item) => {
const key = item.category
if (!acc[key]) acc[key] = []
acc[key].push(item)
return acc
}, {})
}
// arrayUtils.test.js
import { describe, it, expect } from 'vitest'
import { filterByStatus, groupByCategory } from './arrayUtils.js'
describe('数组工具函数', () => {
const items = [
{ id: 1, status: 'active', category: 'A' },
{ id: 2, status: 'inactive', category: 'A' },
{ id: 3, status: 'active', category: 'B' },
]
describe('filterByStatus', () => {
it('应筛选出指定状态的项', () => {
const result = filterByStatus(items, 'active')
expect(result).toHaveLength(2)
expect(result.every((item) => item.status === 'active')).toBe(true)
})
it('无匹配时应返回空数组', () => {
const result = filterByStatus(items, 'deleted')
expect(result).toHaveLength(0)
})
})
describe('groupByCategory', () => {
it('应按分类分组', () => {
const result = groupByCategory(items)
expect(result).toHaveProperty('A')
expect(result).toHaveProperty('B')
expect(result.A).toHaveLength(2)
expect(result.B).toHaveLength(1)
})
})
})
异步测试和钩子函数
真实项目里大部分代码都是异步的——查数据库、调 API、读文件。框架都支持 async/await,写法很简单:
// Mocha 异步测试
it('应异步获取用户列表', async () => {
const users = await getUsers()
expect(users).to.be.an('array')
expect(users.length).to.be.greaterThan(0)
})
钩子函数让你在测试前后做准备工作:
describe('数据库相关测试', () => {
before(async () => {
// 所有测试开始前,连一次数据库
await db.connect()
})
after(async () => {
// 所有测试结束后,断开连接
await db.close()
})
beforeEach(() => {
// 每个测试前,清空数据,保证独立
db.clear()
})
it('应正确创建用户', async () => {
const user = await db.users.create({ name: '测试用户' })
expect(user.id).to.exist
})
})
Mock 和 Stub:不用真调也能测
单元测试最大的难点不是怎么写 assert,而是怎么隔离依赖。
你的订单处理函数依赖数据库、缓存、支付接口、发短信的第三方 API。如果你每个测试都去真实调用这些东西:
- 慢。数据库查询几十毫秒,积少成多。
- 不稳定。外部 API 挂了你的测试也挂,但你代码没问题。
- 没法测异常。你怎么让支付宝真的返回一个"余额不足"?
这时候你需要 Mock 和 Stub。
区别很简单:
- Stub:替换一个函数,让它返回你指定的值。像考试时你抄了一份标准答案。
- Mock:不仅替换返回值,还能验证这个函数被调用了几次、传了什么参数。像找了一个替考,考完还能告诉你他是什么时候交卷的。
来,实战一个:
// userService.js
export async function getUserWithPosts(userId) {
const user = await fetchUser(userId)
const posts = await fetchPosts(userId)
return { user, posts }
}
// userService.test.js (使用 Jest Mock)
import { getUserWithPosts } from './userService.js'
import { fetchUser, fetchPosts } from './api.js'
jest.mock('./api.js')
describe('getUserWithPosts', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('应返回用户及其文章', async () => {
// Stub:预设返回值
fetchUser.mockResolvedValue({ id: 1, name: '张三' })
fetchPosts.mockResolvedValue([{ id: 1, title: '第一篇' }])
const result = await getUserWithPosts(1)
expect(result.user.name).toBe('张三')
expect(result.posts).toHaveLength(1)
// Mock验证:调用参数对不对
expect(fetchUser).toHaveBeenCalledWith(1)
expect(fetchPosts).toHaveBeenCalledWith(1)
})
it('API 错误时应抛出异常', async () => {
fetchUser.mockRejectedValue(new Error('网络错误'))
await expect(getUserWithPosts(1)).rejects.toThrow('网络错误')
})
})
看到了吗?Mock 让你可以模拟任何场景。API 成功、API 失败、返回空数据、超时——所有异常路径都能测到,不用真的去拔网线。
第二部分:性能测试——你的系统能不能扛住
单元测试管对错,性能测试管生死
单元测试问你:这个函数逻辑对不对?
性能测试问你:这个系统扛不扛得住?
想象你造了一辆车。单元测试是确保四个轮子能转、刹车能停、方向盘能打。性能测试是把它开到高速上,油门踩到底,看看水箱会不会炸。
性能测试通过模拟大量请求,测量四个核心指标:
- 响应时间:一个请求等多久。看 P50、P90、P99 —— 不是你测两次取平均值,而是看最慢的那批。
- 吞吐量(TPS/QPS):每秒能处理多少请求。
- 资源利用率:CPU 吃多少,内存吃多少,网卡跑满没有。
- 错误率:有多少请求直接挂了。
它回答几个致命问题:
- 系统能撑多少并发用户?1000 个还是 100 个?
- 大促时候会不会崩?
- 跑着跑着内存是不是在涨?——这就是内存泄漏。
- 高峰流量过了之后系统能恢复吗?
性能测试不是一种,是四种
很多人说"做压测",其实说的就是一顿乱打。真正的性能测试有四种,每一种目的不同:
第一,基准测试(Baseline)
干啥的:在低负载下跑一次,拿到一个"正常情况"的数据。以后每次发版跑一次,对比一下。如果数字变差了,说明你这版代码引入了性能退化。
怎么跑:单用户或几个用户,跑 5-10 分钟。
看什么:单用户平均响应时间、基础资源消耗。
第二,容量测试(Load Test)
干啥的:这就是真正的"压测"。从低负载开始,逐步加大并发,找到系统的天花板。
什么时候做:大促前、新功能上线前、想知道你系统到底能撑多少用户。
怎么跑:一步步加并发。比如从 10 用户开始,每 30 秒加 10 个,直到响应时间突然暴涨或者错误率飙升。那个点就是你的性能拐点。
看什么:最大 TPS、不同并发下的响应时间分布、拐点在哪里。
第三,稳定性测试(Endurance Test)
干啥的:让系统在 80% 容量的负载下,连续跑 8 小时、24 小时、甚至 7 天。目的是抓慢性病——内存泄漏、连接泄漏、GC 异常。
什么时候做:你的服务是 7x24 小时运行的。你说你代码没问题——跑三天看看,内存曲线是不是悄悄往上爬。
必须盯着的指标:
- 内存使用趋势:是不是在涨?涨了就是泄漏。
- CPU 使用趋势:是不是稳定?突然飙高说明有定时任务或批处理有问题。
- 句柄数:是不是在涨?涨了就是你没关连接。
- GC 频率:是不是越来越频繁?说明内存管理出问题了。
- 错误日志:是不是在缓慢累加?
第四,异常测试(Stress Test / Spike Test)
干啥的:模拟突发流量。比如秒杀开始那一刻,流量瞬间飙升 10 倍。
什么时候做:秒杀活动前、抢票系统、或者你想验证你的限流降级机制到底有没有用。
怎么跑:从正常负载瞬间拉到峰值,持续几秒,再降下来。看系统能不能扛住峰值(或者优雅降级),峰值过后能不能恢复。
什么场景必须做性能测试?
不是每个项目都需要完整的四类测试,但下面这些场景,不做就是在拿上线当天赌命:
| 场景 | 为什么必须做 |
|---|---|
| 新应用上线 | 你不知道它能撑多少量——你的估算大概率是错的 |
| 核心高频服务 | 支付、订单、秒杀。挂了就是钱 |
| 大促活动 | 双十一、演唱会抢票。不提前压测就是开盲盒 |
| 架构调整后 | 微服务拆分、数据库迁移。架构改了性能一定会变 |
| 长期运行服务 | 内存泄漏这种慢性病,不跑稳定性测试发现不了 |
说几个血淋淋的例子:
- 某电商双十一没压测,开卖第一分钟数据库直接打满,整个平台瘫痪 20 分钟。
- 某银行系统升级后没做稳定性测试,运行三天后内存泄漏导致重启,丢了几百笔交易。
- 某抢票系统最火的时候崩了,直接损失几百万。
这些团队不是因为技术不行,是因为偷懒。
性能测试的完整流程
做性能测试别一上来就 artillery run。先想清楚你要测什么、怎么测、怎么判断结果。
第一步:定目标
你要 TPS 多少?P95 响应时间多少毫秒?错误率多少以下?不知道就去跟业务确认。没有目标的测试是瞎跑。
第二步:建模型
不是随便打几个请求就叫压测了。你得想清楚你的用户都在干嘛:
- 80% 的人在浏览商品
- 15% 的人在下单
- 5% 的人在搜索
按这个比例设计你的测试脚本。用真实数据分布,别用 "username": "test" 这种假数据——生产环境的数据量级和分布完全不一样。
第三步:选工具
下面马上讲工具对比。
第四步:架上监控
压测工具告诉你"系统慢了",但它不告诉你"哪里慢"。你需要 Prometheus + Grafana 来看 CPU、内存、网络、磁盘。最好加上 APM(应用性能监控)来看是哪个函数慢、哪个 SQL 慢。
第五步:执行
从低负载开始,慢慢加。别一上来就打满。每一步都要观察指标,发现异常立刻记录位置。
第六步:分析输出报告
对比目标和实际结果。定位瓶颈。给出优化建议。如果没达标,优化后重新跑。如果达标了,记录结果作为下次的基准。
工具选谁:Artillery vs k6
性能测试工具也很多,但 Node.js 生态里最常用的就两个。
Artillery:Node.js 亲生的
Artillery 用 YAML 写测试场景,配置简单到你觉得不真实。但就是好用。
npm install -g artillery
一个基础配置长这样:
# test.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 20
name: 'Sustained load'
scenarios:
- name: '获取用户列表'
flow:
- get:
url: '/api/users'
capture:
- json: '$.data[0].id'
as: 'userId'
- get:
url: '/api/users/{{ userId }}'
artillery run test.yml
输出报告会告诉你:请求发了多少、平均响应时间多少、P95 多少、P99 多少。一目了然。
高级用法支持 CSV 参数化、思考时间(模拟用户在页面上发呆)、自定义断言(确保返回了 200)。
config:
target: 'http://my-api.com'
phases:
- duration: 300
arrivalRate: 20
rampTo: 50
payload:
path: 'users.csv'
fields:
- 'userId'
- 'token'
order: 'random'
scenarios:
- name: '完整用户流程'
flow:
- post:
url: '/api/auth/login'
json:
userId: '{{ userId }}'
token: '{{ token }}'
capture:
- json: '$.token'
as: 'authToken'
expect:
- statusCode: 200
- think: 2
- get:
url: '/api/users/me'
headers:
Authorization: 'Bearer {{ authToken }}'
k6:用 JavaScript 写压测脚本
k6 是用 Go 写的,性能强。脚本用 JavaScript 写,上手门槛极低。
# macOS
brew install k6
# Linux
curl -s https://github.com/grafana/k6/releases/latest/download/k6-latest-linux-amd64.deb -o k6.deb
sudo dpkg -i k6.deb
一个最小脚本:
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
stages: [
{ duration: '1m', target: 10 }, // 1 分钟加到 10 用户
{ duration: '3m', target: 10 }, // 维持 3 分钟
{ duration: '1m', target: 0 }, // 1 分钟降到 0
],
thresholds: {
http_req_duration: ['p(95)<500'], // P95 小于 500ms
http_req_failed: ['rate<0.01'], // 错误率低于 1%
},
}
export default function () {
const res = http.get('http://localhost:3000/api/users')
check(res, {
'status is 200': (r) => r.status === 200,
})
sleep(1)
}
跑起来:
k6 run script.js
输出是终端里的实时表格——current TPS、平均响应时间、P95、错误数。看着它跑,有种开赛车的感觉。
k6 还能接 InfluxDB + Grafana 做实时仪表盘。压测的时候打开 Grafana,看着曲线往上爬,等拐点出现的那一刻——你就知道你的系统上限在哪了。
我怎么选?
- 简单的 HTTP 接口压测、团队里会写 YAML 的比会写 JS 的多——选 Artillery。
- 需要复杂场景编排、自定义指标、CI/CD 集成——选 k6。
- 两个都不难,花半天就能上手。
加上 APM:知道"哪里慢"
压测工具告诉你了"P95 响应时间 850ms,太慢了"。然后呢?你盯着日志,不知道哪里慢。
这时候你需要 APM。它给你的请求画一张"账本"——从进来到出去,每一步花了多久。
比方说:
- API Gateway 花了 10ms
- 你的 Node.js 服务 花了 50ms
- 数据库查询花了 800ms
看到了吧。问题根本不在你的代码,而是数据库没有索引。
常用 APM 工具:
| 工具 | 类型 | 特点 |
|---|---|---|
| New Relic | 商业 | 全面,端到端追踪,贵 |
| Datadog | 商业 | 云原生,集成能力强 |
| Elastic APM | 开源 | 跟 ELK 栈绑在一起 |
| Prometheus + OpenTelemetry | 开源 | 完全自己搭,免费但费人 |
如果你刚开始搞,用 New Relic 或者 Datadog 的免费 tier 就行。先把流程跑通,再考虑自建。
性能测试 Checklist
每次做之前,过一遍:
测试前准备
- 明确性能目标(TPS、响应时间、错误率)——别上来就跑
- 准备真实测试数据——不要用假数据
- 测试环境尽量接近生产环境——机器的差异会骗你
- 部署好监控系统——跑完不知道结果等于白跑
- 编写并审查测试脚本——脚本写错了测出来的数据没意义
测试执行
- 从低负载开始,逐步加压——别一上来打满
- 每一步都观察指标变化——找拐点
- 记录每一次异常——回头看的时候有用
- 多跑几次验证一致性——排除偶发因素
测试后分析
- 对比目标与实际——过没过
- 定位瓶颈——哪里卡了
- 输出优化建议——怎么改
- 存档作为基准——下次发版回来比
第三部分:把测试塞进 CI/CD
本地跑了还不够
你可能会问:"我本地测过了,为啥还要在 CI 里跑?"
因为你自己不能代表所有人。别人改了一行代码,合并进来,你的模块挂了。你自己不知道,因为你没拉最新代码。但 CI 会知道——它会在合并的入口挡下来。
把测试放进 CI 的三层意义:
- 拦截:CI 不过就不能合,守住质量底线。
- 监控:每次 push 都在跑,代码变更跟测试结果强绑定。
- 解放:你不用每次在本地跑全套测试了,CI 帮你跑,你去看结果就行。
GitHub Actions 配置
一个最简配置,扔在 .github/workflows/test.yml 里:
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm test
- uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
performance-test:
runs-on: ubuntu-latest
needs: unit-test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run start:prod &
- run: npx k6 run tests/performance.js
注意:单元测试过了才跑性能测试(needs: unit-test)。如果代码逻辑都错了,压测没意义。
给你一套判断框架
写到这儿,我不再重复前面说过的东西了。你不需要总结,你需要的是:以后每次面对测试问题的时候,自己能判断该怎么做。下面几个问题你掂量一下:
单元测试方面:
- 这个函数是不是核心路径?如果它坏了,会不会导致线上崩?——会,就测。
- 这个函数的边界条件我想过吗?——没想过,写完测试你就想过了。
- 我敢不敢直接改这段代码然后上线?——不敢,说明你缺测试。
- 覆盖率 85% 了,但剩下的 15% 是不是真的重要?——不重要就别追,数字是瞎的。
- 我是用什么框架写的?Vite 就用 Vitest,React 用 Jest,老项目用 Mocha。别纠结。
性能测试方面:
- 我的系统要面对多少用户?有没有高峰期?——不知道就去问。有高峰期就压测。
- 什么时候做测试?——上线前、大促前、架构改动后。这三次必须做。
- 做哪种?——基准测试每次都跑,容量测试上线前跑,稳定性测试长期服务跑,异常测试秒杀场景跑。
- 用什么工具?——简单用 Artillery,复杂用 k6。
- 慢在哪儿?——工具只告诉你"慢了",APM 告诉你"哪儿慢了"。
流程方面:
- 测试进 CI 了吗?——没进的话,你本地的测试等于摆设。
- CI 不过能合并吗?——能合并的话,CI 等于摆设。
- 上一次压测的结果在哪儿?——找不到的话,你这次跑了也白跑。
测试不是仪式。测试是你对这个系统的信任。信任不是靠感觉建立的,是靠每次代码变更后那排绿色的 ✓。
把测试写好,不是为了老板,是为了将来那个凌晨三点在被窝里不用接电话的你。
读者来信
暂无来信,期待你的分享。