jest + electron 基础实践——jest-electron

做者 hustcc 蚂蚁金服·数据体验技术团队html

tl;dr 项目地址 jest-electron前端

1、背景

目前社区上最火热 / 流行的单测框架,必然是 jest。咱们前端写单测遇到最多的问题是什么?那必然是没法模拟出真实的浏览器环境。好比:node

  • 依赖 dom API 的模块和方法
    • UI 组件
    • canvas 画布
    • ...
  • 依赖浏览器控制台调试
    • 看 UI 表现
    • 交互过程动画
    • 辅助写单测断言语句

这就是 jest-electron 要作的事情,将 jest 的单测代码放到 electron(底层是 chrome)中去跑,而且能够在 electron 中进行熟悉的前端调试。git

2、实现原理

一句话来讲,就是经过自定义 jest 的 runner,在这个自定义 runner 中,启动 electron 进程,而后将单测代码的逻辑放到 electron 进程中去跑,最后返回结果。github

分红三步内容介绍:web

  1. electron
  2. jest runner
  3. 组合能力 -> jest-electron

2.1 electron

我的以为整体上,electron 的架构和能力仍是很清晰明了的,并不会让人以为晦涩难懂。chrome

简介

Electron 是由 Github 开发,用 HTML,CSS 和 JavaScript 来构建跨平台桌面应用程序的一个开源库。 Electron 经过将 Chromium 和 Node.js 合并到同一个运行时环境中,并将其打包为 Mac,Windows 和 Linux 系统下的应用来实现这一目的。typescript

image.png

main & renderer

咱们从 electron 的使用方式来简单窥探一下。npm

electron index.jscanvas

启动以后,就弹出框。

image.png

看 demo 的代码目录其实能够很清晰的看到,代码分红两部分,一部分是 main ,一个部分是 renderer 。怎么区分:

  • main 是在 electron 启动入口文件 index.js 中所有加载的
  • renderer 是在 BrowserWindow 中 load 进去的 html 中加载的

这就是 electron 两个很是重要的概念了。弄懂 main 进程和 render 进程,以及他们以前的通讯方式,基本上 electron 的使用就是查 API 了。

image.png

简单概括一下:

  • main 运行于 node 环境,能够运行获取数据,存储数据等 API
  • renderer 运行于浏览器环境, 可使用 HTML、CSS、JS 套件作 UI 展现数据

他们之间经过 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),指至少两个进程或线程间传送数据或信号的一些技术或方法。

2.2 Jest 自定义 runner

本质上是 jest 将运行单测抽出为 runner 模块,这个 runner 实际是一个 class 类,而且其中只有一个方法 runTests。

runner 的职责是:

  1. 根据用户的 jest 配置决定如何运行全部的单测文件:并行、串行、worker 数量等
  2. 读取文件,根据用户的 preset、transform 等配置,编译源文件
  3. 执行单测,收集 TestResults 数据,包含成功失败、覆盖率等

而后 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…

2.3 Jest + Electron

了解了 electron 的使用方式,以及 jest 自定义 runner 的方式。剩下的就是组合逻辑了。

原理

基本的思路是:

  1. 在 jest 自定义 runner 的 runTests 函数中,启动 electron,建立 main 进程
  2. 在 main 进程中建立 BrowserWindow 实例,建立 renderer 进程
  3. runTests 中逐一处理单测数据,将单测数据经过 nodejs 的 process ipc 机制发送到 main 进程中
  4. main 进程经过 electron ipc 通讯机制,将单测发送到 renderer 进程
  5. renderer 进程执行单测数据,获取测试结果 TestResults
  6. TestResults 原路返回到 jest

一图胜千言:

image.png

具体的实现逻辑,仍是看代码吧!

性能优化:multi-renderer

从实现原理来看,要优化性能,其实没有不少的入手的地方,毕竟只是 jest + electron 的包皮层。

可能惟一能够优化的地方在于利用多 cpu 的计算能力,并发运行多个单测文件。

上述介绍 electron 的知道,一个 main 进程对应多个 renderer 进程,而实际运行单测的环境就是在 renderer 中,因此,咱们能够建立一个多 renderer 进程池子

具体实现使用一个 ProcPool 来存储具体 renderer 进程实例,以及它们的是否空闲的状态。运行单测文件的时候,从池子里面取一个 idle 状态的进程,若是不存在则建立一个新的 renderer 进程,同时放入到池子中;运行单测以前将进程状态改为运行中,单测执行完成以后,将进程状态设置为 idle,以便复用。

优化以后测试的效果能够直接看 PR:github.com/hustcc/jest…,结论:

  • jest no-cache 状况下,运行时间下降到以前的 54.5%
  • jest 状况下,运行时间下降到以前的 36.2%

3、使用方式

直接看 GitHub 上的 README.md,使用很是简单,不阻断常规的 Jest 使用。仅支持 Jest 24 版本。

  • 添加 dev 依赖

tnpm i --save-dev jest-electron

  • 修改 jest 配置
{
  "jest": {
+ "runner": "jest-electron/runner",
+ "testEnvironment": "jest-electron/environment"
  }
}
复制代码

就这样就行了,剩下的就是 jest 怎么用就怎么用就好了。

4、一些问题记录

为了提高调试的体验,增长的一些功能和解法。

刷新从新运行

这个运行逻辑是:

  • jest-cli 发送测试,执行 runner 运行
  • runner 将测试发送给 main 进程
  • main 进程找到空闲的 renderer 进程,执行单测
  • jest-cli 获取测试结果显示在 cli 中

那么刷新从新运行的解法就是:

  • main 中运行缓存获取的 tests 数据,而后和对应的 BrowserWindow 关联起来
  • renderer 页面一旦加载成功的时候,发送消息给 main,让 main 将 tests 逐一发送给 renderer 从新运行一遍
  • 当 cli 要给 main 发送 tests 的时候,清空 main 中缓存的 tests 数据,防止重复

electron 控制台打印

由于 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。

5、相关轮子

其实单纯学习 electron 造轮子,没啥必要作竞品调研。这里就当相关项目介绍吧。

问题:

  1. 覆盖率没法收集
  2. 调试的 sourcemap 不正确
  3. 能力不足(typescript、less、svg 等都不支持)
  4. 非 jest 生态

后来咱们队这个作了一个迭代,增长了 ts 等的支持,可是毕竟非 jest 生态,并且 sourcemap、coverage 问题依然没法解决。

抄了一些代码,可是问题:

  1. 没法保持窗口调试
  2. 运行单测慢(单 main、当 renderer)
  3. 代码晦涩

对咱们团队感兴趣的能够关注专栏,关注github或者发送简历至'tao.qit####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相关文章
相关标签/搜索