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

发布于 2024-09-15 22:42:45 字数 8569 浏览 62 评论 0

项目地址 jest-electron

一、背景

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

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

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

二、实现原理

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

分成三步内容介绍:

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

2.1 electron

个人觉得总体上,electron 的架构和能力还是很清晰明了的,并不会让人觉得晦涩难懂。

简介

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

image.png

main & renderer

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

electron index.js

启动之后,就弹出框。

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 配置: https://jestjs.io/docs/en/configuration

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: https://github.com/hustcc/jest-electron/pull/1 ,结论:

  • jest no-cache 情况下,运行时间降低到之前的 54.5%
  • jest 情况下,运行时间降低到之前的 36.2%

三、使用方式

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

添加 dev 依赖

tnpm i --save-dev jest-electron

修改 jest 配置

{
  "jest": {
+    "runner": "jest-electron/runner",
+    "testEnvironment": "jest-electron/environment"
  }
}

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

四、一些问题记录

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

刷新重新运行

这个运行逻辑是:

  • 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
      Object.defineProperty(this.global, 'console', {
        get: () => {
          return this.electronWindowConsole;
        },
        set: () => {/* do nothing. */},
      });

      installCommonGlobals(this.global, config.globals);
    } catch (e) {}
  }
}

通过 defineProperty 强制无法覆盖属性,获取属性的时候,直接使用 electron 浏览器环境的 console。

五、相关轮子

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

torchjs

问题:

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

后来我们队这个做了一个迭代,增加了 ts 等的支持,但是毕竟非 jest 生态,而且 sourcemap、coverage 问题依然无法解决。

jest-runner/electron

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

扭转时空

暂无简介

文章
评论
27 人气
更多

推荐作者

初吻给了烟

文章 0 评论 0

十二

文章 0 评论 0

清引

文章 0 评论 0

丢了幸福的猪

文章 0 评论 0

微信用户

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文