JS单元测试关键在断言逻辑、异步处理、副作用隔离和环境模拟;需聚焦行为意图而非固定值,显式处理异步,隔离副作用,最小化mock以保障契约验证。

JS 单元测试不是写完 test() 就算通过,真正容易出问题的,是断言逻辑、异步处理、副作用隔离和测试环境模拟这几个环节。
断言别只看“值相等”,要盯住“行为意图”
很多同学用 expect(result).toBe(42) 过早锁定具体值,结果函数内部重构(比如改用缓存、换算法)导致测试失败,但业务行为其实没变。关键不是“结果是不是 42”,而是“它是否按约定返回了正确类型、是否触发了预期副作用、是否在错误输入下抛出合适错误”。
- 对返回对象,优先用
toEqual比较结构,或用toHaveProperty验证关键字段存在性和类型 - 对函数调用,用
toHaveBeenCalledTimes和toHaveBeenCalledWith检查是否被调用、传参是否符合契约 - 对错误场景,用
expect(() => fn()).toThrow(/invalid/)并匹配错误消息关键词,而非整个字符串
异步代码必须显式“交还控制权”
忘记 await、漏写 done()、或在 setTimeout 里断言却没等定时器执行——这三类问题占异步测试失败的 70% 以上。Jest 不会自动等待你手写的 Promise 或定时器,它只认你明确告诉它的异步边界。
- 使用
async/await是最安全方式:test('fetches user', async () => { const user = await fetchUser(); expect(user.name).toBe('Alice'); }); - 若用回调风格(如老版 Node.js API),必须传
done参数并在回调末尾调用done(),且错误时调用done.fail() - 避免在测试中直接用
setTimeout;改用jest.useFakeTimers()+jest.runAllTimers()精确控制时间流
副作用必须隔离,别让测试互相污染
全局变量修改、DOM 操作、localStorage 写入、定时器未清理……这些都会让测试从“独立单元”变成“状态依赖链条”。一个测试清空了 localStorage,下一个测试就拿不到初始数据,报错却和它自己代码无关。
- 每个测试前用
beforeEach(() => { jest.clearAllMocks(); })重置 mock 状态 - 修改全局对象(如
Date.now)后,务必在afterEach中恢复,或用jest.mock替换整个模块 - 操作 DOM 的测试,用
document.body.innerHTML = ''清空,或搭配@testing-library/dom的cleanup() - 启动了定时器或事件监听器的,记得在
afterEach中清除(clearTimeout/removeEventListener)
Mock 要“最小够用”,别过度替换真实逻辑
看到 HTTP 请求就 jest.mock('axios'),看到工具函数就 jest.mock('./utils')——看似省事,实则掩盖了接口变更风险。Mock 应该只覆盖不可控外部依赖(如网络、文件系统),而不是把本该被测的逻辑也挡在外面。
- 优先用
jest.fn()手动 mock 具体函数,而不是整模块 mock;这样能精准控制返回值,也保留了对调用次数/参数的验证能力 - 对第三方库,用
__mocks__/目录做轻量模拟(比如只 mockaxios.get返回固定数据),不 mock 整个axios对象 - 真实模块中有纯函数逻辑?别 mock 它,让它参与测试——这才是单元测试的价值:验证你自己写的代码在各种输入下是否可靠
防坑的核心不是记住所有规则,而是每次写测试时多问一句:这个断言真的在保护我的业务契约吗?这个 mock 是在隔离外部不确定性,还是在绕过我该负责的逻辑?


















