问题简述
因为之前 node jest 一直有 内存泄漏的问题(其实不是内存泄漏),所以研究了下jest 为什么会有这个问题。以及 node 现在 一直runInBand (单个运行),限制了运行的速度。从jest 以及 ts-jest 运行源码出发分析问题。因为只是 从内存方面来理解jest的,所以有很多地方并没有细看,下面在介绍jest的时候 可能不会很全面
jest 简述(和其他测试框架:mocha 比较)
测试框架 , 类似的还有mocha 。 但是jest 明显比mocha 复杂数倍 。mocha 只是基本的测试框架提供了简单的 测试api , jest 可以看作是一个完整的 代码(不止js)运行环境,可以做到编译级别的代码控制
jest 框架 简述 (不考虑 watch 和 覆盖率 模式)
从 cli : jest --config jest.config.json 开始 的运行 步骤
步骤 (function, class等) | 步骤内容 |
---|---|
run() | cli 运行,会在代码里调用这个 function ,主要用来build args 和 获取一些 环境args |
runCLI() | 这里在获取 输入的args之后,jest 会init config ,从环境变量和 命令指定的dir 或者文件 获取 config , 并且会校验和填充一些args |
_run10000 () : 就是这么命名的 | 创建context ,并且会保存到缓存文件(默认:/tmp/jest_0/ 目录 , linux 系统) , 并且会用 输入的参数做 测试文件filter ,然后是 运行watch 或者 非 watch 模式 |
![]() |
|
runWithoutWatch() | 运行非watch模式 jest |
runJest () | 对test 文件进行处理:排序 , 搜索 。 运行 global hook , init jest 输出 console ,然后 计划 运行 test , 最后对 运行的测试results进行缓存(这个缓存只是为了给 前面的排序使用)![]() |
TestScheduler class:测试调度 | test的 调度 class , 内部使用 scheduleTests 运行测试,同时负责 reporter init 和 reporter的 event 分发(emit) |
scheduleTests() | init 测试 数据:测试的empty results , 多线程运行(runInBand),测试运行的 event 分发事件绑定(onResult , onFailure 等) 。 使用代码transform 导入 源文件,init 运行测试的Runner (jest-runner ),对 对应的测试绑定对应的 runner 以及 runner的运行 event (test-file-start/success/failure 等) 。 然后开始运行 tests。运行之后 收集 test 运行 results 进行 合并 以及 对应的event 分发(emit) |
Runner.runTests() : Runner 默认 jest-runner | 多线程或者 单线程 运行tests (runInBand , 这里只 说明 单线程) |
_createInBandTestRun() | 分发 test 文件运行的各种 event (test-file-* , onResult , onFailure 等),运行 test (到这里 就是一个 test 文件 ,因为是 runInBand),for 循环运行test |
runTest() | 运行测试,可以检测内存泄漏(leakDetector) |
runTestInternal(): 测试开始运行的核心代码 | 加载测试必须的核心文件 class : TestEnvironment , TestFramework , Runtime 并且init ,还会检测内存泄漏 和 运行代码覆盖率 。如果使用 gc,会在test 运行之后 执行 gc ,如果使用 logHeapUsage , 会用node 的api 显示 heap 的使用 , 运行之后会对 TestEnvironment clear 注意 加载文件使用 config 配置的 transform ,这也是一个class ,下面讲下 加载非test 和 test 依赖文件的方式在加载文件的时候 , jest 会拦截 require ,然后判断是否需要 做 transform 。如果需要transform,就用transform逻辑。外部使用 requireAndTranspileModule()核心文件 transfrom的逻辑1. 首先 会check 是否存在缓存文件 |
-
不存在缓存,就用 transform 对应的 文件来做 ts/js→js 的操作
-
然后对获取的文件保存到对应的缓存目录
-
外部使用 requireInternalModule()导入第三方文件 逻辑, 是jest 内部导入的文件1. 对第三文件进行 module 处理,从内存从读取 是否存在
-
不存在 继续导入 对应module ,因为文件可能有其他的依赖,所以不是简单的导入,可以理解为 在代码里做了require , 会依次执行文件依赖。通过 node api :vm 和 script 直接运行 导入的源代码 ,同时拦截 源文件的 moduleapi (commonjs 导入模块的 api ) ,对文件的依赖可以依次处理
-
对导入的文件 进行transform(transfromfile) , 这里使用 transform ,会对transfrom的 源代码保存到 内存中,因为都是js 所以不会用 transform的逻辑 | | testFramework():jestAdapter | 1. 导入 必须源文件 : jestAdapterInit.js , 运行 fun:initialize() , init 测试必须的 一些function 和参数 (it , describe , skip , only等)。分发 测试 event 和 添加一些 handler ,返回 init 之后的 参数
-
运行 global hooks : clear 运行环境
-
requireModule():导入 test文件 注意这里和 requireInternalModule 逻辑类似,但是这列导入的test 文件 可能是 ts 文件,所以会运行 transform 的逻辑(ts-jest)下面说明 transfrom的逻辑区别1. 运行 scriptTransformer 的 transform
-
transform 运行 cli 或者 jest.config.json 配置的 transform(ts-jest)
-
ts-jest 对 ts 文件进行编译 并且 会保存编译文件到缓存目录 ,而且会保存到内存中 (object.create(null) , 无法垃圾回收)
-
ts-jest 返回 编译信息 , jest 会 保存到缓存目录 和 内存中 ***(map , 无法垃圾回收)***transform 之后 源码会在 vm 和 script 内运行 ,里面会注入 jest 全局对象 , 类似 describe 和 it 会 执行 ,这里会分发event 和执行逻辑。然后会在对应的handler内部收集 test 或者 其他的fn 导入到 对应的 数组或 对象内部 ,这里测试代码是 还没有运行的runAndTransfromResultsToJestFormat():用上面收集的 test fun和数据 运行 测试代码 , 就是一个迭代 运行fun ,然后返回测试 运行的results ,jest的api 都已经 注入 拦截能够 使用 依次返回 函数调用迭代 ,完成 测试代码运行 |
分析node 内存问题
从上面的jest 运行能够分析出,每次运行的代码都会在内存 中,因为server代码在运行的时候 是 依赖整个项目执行的,所以每个测试 都会导入不一样的源码 。jest 会保存 一份 在内存中 ,ts-jest 会保存一份
在内存中,而且不能gc,这就是为什么node 运行之后内存会不够用(不是泄漏,process.exit 之后内存就释放了)。
如果要解决这个问题有几个方案
-
大内存运行
-
node 运行测试的时候不编译源码,使用 和前端类似的模式,只获取 http 。对ts 测试预编译,不使用ts-jest ,所以的运行代码都是 js
-
使用多台机器运行 ,直接待用jest api执行 ,减少一台机器的内存使用 (代码已经实现 参考:dev.zstack.io:9080/qing.liang/... jest branch),下面介绍 实现
运行了几个小时之后的 文件缓存,内存缓存 比这个多,还有源码也是保存在内存的


jest 多机器运行
实现思路
通过 启动一个host socket server ,其他多台包括 host 启动 socket client 连接 ,请求 test (从test列表获取) ,然后 client 运行 jest api 返回给 server ,server 对 results 合并,运行完成之后 调用jest html
的api (customer reporter ) 生成 html

下面列出 jest 和 jest html api
function 和 class | 使用 |
---|---|
socket.io | socket class |
readConfigs(): Client.ts | 获取jest 配置 config |
buildArgv(): Client.ts | 从cli 构建 jest args |
runCLI(): Client.ts | 运行 test |
jest-html-reporters class : Server.ts | jest reporter class |
jestReporter.onRunComplete():Server.ts | 生成 html |