做者 hustcc 蚂蚁金服·数据体验技术团队html
tl;dr 项目地址 jest-electron。前端
目前社区上最火热 / 流行的单测框架,必然是
jest
。咱们前端写单测遇到最多的问题是什么?那必然是没法模拟出真实的浏览器环境。好比:node
这就是 jest-electron 要作的事情,将 jest 的单测代码放到 electron(底层是 chrome)中去跑,而且能够在 electron 中进行熟悉的前端调试。git
一句话来讲,就是经过自定义 jest 的 runner,在这个自定义 runner 中,启动 electron 进程,而后将单测代码的逻辑放到 electron 进程中去跑,最后返回结果。github
分红三步内容介绍:web
我的以为整体上,electron 的架构和能力仍是很清晰明了的,并不会让人以为晦涩难懂。chrome
Electron 是由 Github 开发,用 HTML,CSS 和 JavaScript 来构建跨平台桌面应用程序的一个开源库。 Electron 经过将 Chromium 和 Node.js 合并到同一个运行时环境中,并将其打包为 Mac,Windows 和 Linux 系统下的应用来实现这一目的。typescript
咱们从 electron 的使用方式来简单窥探一下。npm
electron index.jscanvas
启动以后,就弹出框。
看 demo 的代码目录其实能够很清晰的看到,代码分红两部分,一部分是 main
,一个部分是 renderer
。怎么区分:
这就是 electron 两个很是重要的概念了。弄懂 main 进程和 render 进程,以及他们以前的通讯方式,基本上 electron 的使用就是查 API 了。
他们之间经过 electron 提供的 ipcMain,ipcRender 两个 ipc API 进行通讯。
这样的架构就和咱们开发 web 应用没有什么差异了。一个数据层、一个 UI 层,中间提供一些通讯机制(web 开发的前端、后端、HTTP 架构)。
ipcMain、ipcRenderer 的 API 都继承自 EventEmitter,因此这些 API 都是很是熟悉的了吧。
// 添加下面的代码。
// 引入 ipcRenderer 模块。
import { ipcRenderer } = 'electron';
document.getElementById('button').onclick = function () {
// 使用 ipcRenderer.send 向主进程发送消息。
ipcRenderer.send('asynchronous-message', 'hello world');
}
// 监听主进程返回的消息
ipcRenderer.on('asynchronous-reply', function (event, arg) {
alert(arg);
});
复制代码
备注:IPC 进程间通讯(Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法。
本质上是 jest 将运行单测抽出为 runner 模块,这个 runner 实际是一个 class 类,而且其中只有一个方法 runTests。
runner 的职责是:
而后 jest 根据 TestResults 显示测试报告。
一个 runner 骨架类:
/** * Runner 类 */
export default class ElectronRunner {
private _globalConfig: any;
constructor(globalConfig: any) {
this._globalConfig = globalConfig;
}
// 自定义 runTests 函数
async runTests(
tests: Array<any>,
watcher: any,
onStart: (Test) => void,
onResult: (Test, TestResult) => void,
onFailure: (Test, Error) => void,
) {
await Promise.all(
tests.map(
throat(concurrency, async test => {
onStart(test);
// 运行单个单测文件
return await runTest({ ... }).then(testResult => {
testResult.failureMessage != null
? onFailure(test, testResult.failureMessage)
: onResult(test, testResult);
}).catch(error => {
return onFailure(test, error);
});
}),
),
);
}
}
复制代码
社区提供了包装,让建立 runner 更加简单:jest-community/create-jest-runner。Jest runner 配置:jestjs.io/docs/en/con…
了解了 electron 的使用方式,以及 jest 自定义 runner 的方式。剩下的就是组合逻辑了。
基本的思路是:
一图胜千言:
具体的实现逻辑,仍是看代码吧!
从实现原理来看,要优化性能,其实没有不少的入手的地方,毕竟只是 jest + electron 的包皮层。
可能惟一能够优化的地方在于利用多 cpu 的计算能力,并发运行多个单测文件。
上述介绍 electron 的知道,一个 main 进程对应多个 renderer 进程,而实际运行单测的环境就是在 renderer 中,因此,咱们能够建立一个多 renderer 进程池子。
具体实现使用一个 ProcPool 来存储具体 renderer 进程实例,以及它们的是否空闲的状态。运行单测文件的时候,从池子里面取一个 idle 状态的进程,若是不存在则建立一个新的 renderer 进程,同时放入到池子中;运行单测以前将进程状态改为运行中,单测执行完成以后,将进程状态设置为 idle,以便复用。
优化以后测试的效果能够直接看 PR:github.com/hustcc/jest…,结论:
直接看 GitHub 上的 README.md,使用很是简单,不阻断常规的 Jest 使用。仅支持 Jest 24 版本。
tnpm i --save-dev jest-electron
{
"jest": {
+ "runner": "jest-electron/runner",
+ "testEnvironment": "jest-electron/environment"
}
}
复制代码
就这样就行了,剩下的就是 jest 怎么用就怎么用就好了。
为了提高调试的体验,增长的一些功能和解法。
这个运行逻辑是:
那么刷新从新运行的解法就是:
由于 jest-runner 这行代码,会默认强制将运行环境中的 console 指定给 jest 本身建立的 BufferConsole 实例,因此单测代码中的 console 语句,均打印到 cli 中了。具体代码以下:
setGlobal(environment.global, 'console', testConsole);
复制代码
由于 这行代码的执行时间,晚于 自定义的 env,因此只能经过在 env 中 defineProperty 的方式来 mock 掉。
export default class ElectronEnvironment {
private electronWindowConsole: any;
constructor(config: any) {
this.electronWindowConsole = global.console;
this.global = global;
// defineProperty multi-times will throw
try {
// 由于 jest runTest 中会强制设置 console,覆盖掉 electron 的 console 实例
// https://github.com/facebook/jest/blob/6e6a8e827bdf392790ac60eb4d4226af3844cb15/packages/jest-runner/src/runTest.ts#L153
Object.defineProperty(this.global, 'console', {
get: () => {
return this.electronWindowConsole;
},
set: () => {/* do nothing. */},
});
installCommonGlobals(this.global, config.globals);
} catch (e) {}
}
}
复制代码
经过 defineProperty 强制没法覆盖属性,获取属性的时候,直接使用 electron 浏览器环境的 console。
其实单纯学习 electron 造轮子,没啥必要作竞品调研。这里就当相关项目介绍吧。
问题:
后来咱们队这个作了一个迭代,增长了 ts 等的支持,可是毕竟非 jest 生态,并且 sourcemap、coverage 问题依然没法解决。
抄了一些代码,可是问题:
对咱们团队感兴趣的能够关注专栏,关注github或者发送简历至'tao.qit####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~