5月31日 15:55

Jest 测试跑得太慢时该从哪些地方优化?

Jest 测试变慢时,先不要急着把所有用例都改成 mock。更稳的做法是先量出慢在哪里,再从运行范围、测试环境、并发、转换缓存和外部依赖几个点逐个处理。通常收益最大的是三件事:只跑相关测试、把不需要 DOM 的用例放到 node 环境、把网络和计时器这类不稳定依赖隔离掉。CI 上还要控制 worker 数量,因为机器核数看起来很多,不代表同时跑满就最快,I/O、转译和内存都会抢资源。

优化前最好先固定基线:记录完整测试耗时、最慢的测试文件、是否开启 coverage、是否每次都重新转译。很多团队感觉“Jest 越来越慢”,实际是新增了 jsdom 用例、覆盖率范围过大、mock 泄漏导致重试,或者 CI 容器内存不足。把这些因素拆开之后,优化才不会变成凭感觉调参数。

js
// jest.config.js module.exports = { testEnvironment: 'node', maxWorkers: process.env.CI ? '50%' : '75%', testTimeout: 5000, cacheDirectory: '<rootDir>/.jest-cache', collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/**/*.stories.{js,jsx,ts,tsx}', '!src/**/*.d.ts' ] };

追问

为什么先用 --runInBand 或 --detectOpenHandles 排查,而不是直接加 maxWorkers?

maxWorkers 只能调度并发,不能解决测试本身卡住的问题。遇到数据库连接没关闭、定时器没清理、Promise 没 await 时,并发越高日志越乱,定位反而更慢。--runInBand 能把问题压成单进程复现,--detectOpenHandles 可以暴露遗留句柄。代价是这两个参数会明显拖慢执行速度,所以适合排查,不适合长期放进默认 CI 命令。边界也要注意:如果问题只在并发下出现,单进程可能复现不了,这时可以先降低 worker 数量,再逐步缩小到具体文件。

testEnvironment 选 node 还是 jsdom,有什么取舍?

纯函数、Node 服务端逻辑、数据转换这类测试应该优先用 node,启动快、内存少,也少了 DOM 模拟层带来的噪音。组件测试、依赖 documentwindow、布局事件的用例才需要 jsdom。踩坑点是有些工具库会偷偷读取浏览器全局对象,如果全局配置成 node,这些用例会突然失败。比较稳的做法是默认 node,只在需要 DOM 的测试文件顶部用 /** @jest-environment jsdom */ 单独声明。这样做的取舍是配置会分散一些,但换来的是大部分非 UI 测试能保持轻量。

覆盖率收集为什么会拖慢 Jest?

覆盖率需要对代码插桩,转译和文件扫描都会增加开销,尤其是 TypeScript 项目和大仓库更明显。日常本地开发可以不默认打开 coverage,只在提交前或 CI 的独立阶段运行。collectCoverageFrom 要排除声明文件、story、mock、生成代码,否则数字看起来完整,实际是在统计无意义文件。边界是核心库、支付、权限这类高风险模块仍然应该保留覆盖率门禁,不能为了速度完全取消。如果覆盖率阶段太慢,可以把单元测试和 coverage 拆成两个 CI job,让开发先拿到基础测试反馈。

Mock 外部依赖会不会让测试失真?

会,所以 mock 要用在边界上,而不是把所有内部逻辑都替换掉。API、时间、随机数、文件系统、第三方 SDK 适合 mock,因为它们慢且不稳定;业务分支和状态变更如果也全 mock,测试就只是在验证 mock 写得对。一个常见坑是 mock 没有在 afterEach 里恢复,导致后面的用例继承了错误状态。可以配合 jest.clearAllMocks()jest.restoreAllMocks(),让用例之间保持隔离。取舍上,少量集成测试仍然要保留真实调用链,只把网络层替换掉,这样才能发现模块之间的契约问题。

watch、onlyChanged 和 CI 命令应该怎么分开?

本地开发追求反馈快,jest --watchjest --onlyChanged 很合适,因为它们只跑和改动相关的测试。CI 追求确定性,应该跑完整测试,并把 worker、coverage、缓存目录固定下来。不要把 test.onlydescribe.only 当成选择性运行方案,它们很容易被误提交。团队里可以加 ESLint 规则或 pre-commit 检查禁止 .only,这比事后排查漏测便宜得多。还有一个边界是单体仓库:只跑 changed 可能漏掉跨包依赖,CI 最好结合依赖图或至少在合并前跑一次全量。

写段代码

json
{ "scripts": { "test": "jest --watch", "test:changed": "jest --onlyChanged", "test:ci": "jest --ci --coverage --maxWorkers=50%", "test:debug": "jest --runInBand --detectOpenHandles" } }

Jest 性能优化的关键不是把命令堆满,而是给不同场景配不同命令。本地要快,CI 要稳,排查要可复现。只要把环境、并发、覆盖率和 mock 边界分清,大多数“测试越来越慢”的问题都能被压回可控范围。真正需要重写测试时,也应该先从最慢、最不稳定、最依赖外部资源的文件开始,而不是把整个测试目录推倒重来。

标签:Jest