做者 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('####', '@'),歡迎有志之士加入~