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…

相關文章
相關標籤/搜索