测试是桌面应用质量的生命线,尤其是当应用需要在多个平台运行、且功能会随着迭代频繁变化时。Electron 的特殊架构——主进程与渲染进程分离——让测试方案需要分层思考:主进程逻辑要当成 Node.js 服务来测,渲染进程则当成常规前端项目测,而端到端测试则必须覆盖整个应用的真实交互。这套组合拳一旦建好,就能在每次发版前用 CI 自动跑完,省去大量人工回归的精力。
15.5.1 单元测试选型
目前 Electron 生态中最主流的测试框架是 Vitest 和 Jest。两者都支持 TypeScript、快照测试、模拟模块,而且都能很好地适配 Electron 的“一部分代码运行在 Node 环境,一部分运行在浏览器环境”的特点。本教程以 Vitest 为例,因为它基于 Vite,与我们的构建工具天然一致,配置更简洁,速度也更快。
安装基础依赖:
npm install -D vitest @vitest/ui happy-dom
happy-dom 是一个轻量级 DOM 实现,用于在测试环境中模拟浏览器 API,比 jsdom 更快,也更适合大多数 Electron 渲染进程模块的测试。
主进程测试与渲染进程测试最好分开配置。推荐在项目根目录下创建 vitest.config.main.ts 和 vitest.config.renderer.ts 两个配置文件,然后通过 package.json 的脚本区分调用。
主进程配置示例(vitest.config.main.ts):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node', // 主进程代码运行在 Node 环境
include: ['src/main/**/*.test.ts'],
},
});
渲染进程配置示例(vitest.config.renderer.ts):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom', // 模拟浏览器环境
include: ['src/renderer/**/*.test.ts', 'src/renderer/**/*.test.tsx'],
},
});
然后在 package.json 中添加脚本:
{
"scripts": {
"test:main": "vitest run --config vitest.config.main.ts",
"test:renderer": "vitest run --config vitest.config.renderer.ts",
"test": "npm run test:main && npm run test:renderer"
}
}
15.5.2 主进程单元测试
主进程的代码通常包含对 Electron 内置模块(app、BrowserWindow、ipcMain)的依赖。在测试环境中,这些模块需要被模拟,否则会因为缺少真正的 Electron 运行时而报错。常见做法有两种:
- 使用
vitest的vi.mock手动模拟整个electron模块。 - 使用社区方案如
electron-vite内置的测试支持,或直接引入@electron/remote这类辅助库(但更推荐前一种,因为它更可控)。
一个典型的 IPC 处理函数测试:
// src/main/ipc-handlers.ts
import { ipcMain } from 'electron';
export function registerHandlers() {
ipcMain.handle('get-app-version', () => {
return '1.0.0';
});
}
测试文件:
// src/main/ipc-handlers.test.ts
import { describe, it, expect, vi } from 'vitest';
// 必须在文件顶部模拟 electron 模块
vi.mock('electron', () => {
const handleMap = new Map();
return {
ipcMain: {
handle: vi.fn((channel, handler) => {
handleMap.set(channel, handler);
}),
},
// 辅助函数,方便在测试中模拟调用
__test__handleMap: handleMap,
};
});
import { registerHandlers } from './ipc-handlers';
import electron from 'electron';
describe('IPC handlers', () => {
it('should return app version', async () => {
registerHandlers();
// 从模拟的 map 中取出注册的回调并调用
const handler = (electron as any).__test__handleMap.get('get-app-version');
const result = await handler();
expect(result).toBe('1.0.0');
});
});
要点:
- 模拟越轻越好,只模拟你实际使用了的方法。
- 对于更复杂的类(如
BrowserWindow),可以返回一个包含必需属性和方法的简单对象,或使用vi.fn()。
15.5.3 渲染进程单元测试
渲染进程的测试本质上和普通的前端组件测试没有区别。你仍然可以使用 Vue Test Utils、React Testing Library 或类似方案,唯一需要注意的是预加载脚本暴露的全局 API(如 window.electronAPI)需要模拟。
以 Vue 3 + Vue Test Utils 为例,测试一个使用了 window.api.send 的组件:
// src/renderer/components/VersionButton.test.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
// 模拟 window.api
window.api = {
send: vi.fn(),
receive: vi.fn(),
};
import VersionButton from './VersionButton.vue';
describe('VersionButton', () => {
it('should call api.send when clicked', async () => {
const wrapper = mount(VersionButton);
await wrapper.find('button').trigger('click');
expect(window.api.send).toHaveBeenCalledWith('get-version');
});
});
如果你的预加载脚本在实际运行时会做类型检查或复杂的 Proxy 封装,可以通过 vi.stubGlobal 或直接赋值 window.xxx 的方式来做模拟,保持测试简单直接。
对于工具函数、状态管理逻辑(Pinia / Vuex),直接导入测试即可,完全不需要特殊处理。这正是多进程分离带来的好处:界面逻辑与系统能力解耦,测试起来非常纯粹。
15.5.4 端到端测试方案
端到端(E2E)测试的目标是验证整个应用从启动到业务流程完成的完整通路,包括多窗口、原生对话框、托盘菜单等真实场景。当前社区最推荐的方案是 Playwright + Electron,因为 Playwright 原生支持与 Electron 应用的通信,而且其 API 统一、调试体验极佳。
15.5.4.1 安装与配置
npm install -D @playwright/test electron playwright
Playwright 的官方 Electron 支持需要通过 electron.launch 的方式启动应用。在测试文件中,你可以直接创建一个 Electron 应用实例,然后像测试普通网页一样操作窗口。
典型的 Playwright 配置(playwright.config.ts):
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30_000,
expect: {
timeout: 5_000,
},
// 不指定 projects,因为我们只测 Electron,不需要多浏览器
});
15.5.4.2 编写测试用例
首先,我们需要一个工具函数来启动 Electron 应用并获得 Playwright 的 Page 对象。
创建 e2e/helpers.ts:
import { _electron as electron } from 'playwright';
import { ElectronApplication, Page } from '@playwright/test';
export async function launchApp(): Promise<{ app: ElectronApplication; page: Page }> {
// 启动 Electron 应用,main.js 是主进程入口
const app = await electron.launch({
args: ['.'], // 指向项目根目录,Electron 会查找 package.json 中的 main 字段
});
// 等待主窗口出现
const page = await app.firstWindow();
return { app, page };
}
测试示例(e2e/app.spec.ts):
import { test, expect } from '@playwright/test';
import { launchApp } from './helpers';
test('应用启动并显示标题', async () => {
const { app, page } = await launchApp();
// 检查主窗口标题
await expect(page).toHaveTitle('My App');
// 点击某个按钮
const btn = page.locator('button#check-version');
await btn.click();
// 验证弹窗或文案变化
const versionLabel = page.locator('#version');
await expect(versionLabel).toHaveText('v1.0.0');
await app.close();
});
test('文件对话框交互', async () => {
const { app, page } = await launchApp();
// 点击“打开文件”按钮
await page.click('button#open-file');
// Playwright 会自动处理原生对话框吗?
// Electron 的 dialog.showOpenDialog 是原生对话框,Playwright 无法直接操作。
// 需要用 app.evaluate() 在主进程中预先 mock 或监听。
// 这里演示一个更可靠的做法:通过 IPC 暴露方法供测试时调用。
await app.close();
});
处理原生对话框的关键技巧:由于 Playwright 运行在测试进程,无法直接操作系统级对话框,所以我们需要在测试模式下通过 IPC 注入控制。
在应用主进程中,可以增加一个仅用于测试的 IPC 通道:
// main.js 中
if (process.env.NODE_ENV === 'test') {
ipcMain.handle('test:mock-dialog', async () => {
// 返回一个模拟的文件路径,绕过真实的系统对话框
return '/fake/path/to/file.txt';
});
}
然后在测试代码中使用 app.evaluate() 在主进程环境执行代码:
test('文件对话框交互(mock)', async () => {
const { app, page } = await launchApp();
// 在主进程中触发 Mock
await app.evaluate(({ ipcMain }) => {
ipcMain.removeHandler('dialog:open');
ipcMain.handle('dialog:open', async () => {
return { filePaths: ['/test/file.txt'], canceled: false };
});
});
await page.click('button#open-file');
const pathDisplay = page.locator('#file-path');
await expect(pathDisplay).toHaveText('/test/file.txt');
await app.close();
});
15.5.4.3 测试系统托盘和菜单
托盘和菜单的交互更偏向主进程逻辑,可以用 app.evaluate() 直接测试相关模块的函数返回值,而不必真的让测试环境去点击系统托盘图标(这在不同操作系统上行为不一致且不可靠)。将核心逻辑抽象为可测试的纯函数,然后通过 E2E 验证应用启动后托盘图标是否正常创建,菜单项点击是否触发对应的 IPC 即可。
15.5.4.4 CI 集成
在 GitHub Actions 等 CI 环境中运行 E2E 测试时,需要注意 Electron 需要图形环境。Linux 服务器通常没有显示器,此时必须使用 xvfb-run 来提供虚拟帧缓冲。Playwright 已经内置了对 xvfb 的支持,只需在 CI 脚本中直接运行 npx playwright test 即可。确保 CI 运行环境安装了必要的系统依赖(如 libgtk-3-0、libnotify4 等),Playwright 官方文档提供了针对各 Linux 发行版的安装指引。
15.5.5 测试方案选择速查表
| 测试类型 | 工具 | 适用场景 |
|---------|------|---------|
| 主进程单元测试 | Vitest + vi.mock | IPC 处理函数、菜单构建器、数据库操作、文件系统封装 |
| 渲染进程单元测试 | Vitest + Testing Library / Vue Test Utils | 组件交互、状态管理、工具函数 |
| 预加载脚本测试 | Vitest + 模拟 contextBridge | 安全桥接函数、通信协议验证 |
| 端到端测试 | Playwright + Electron | 完整用户流程、窗口管理、异常恢复、跨进程协同 |
将这三层测试组合起来,你可以在提交代码前用单元测试快速反馈逻辑正确性,在合并主干前用 E2E 测试保证业务链条的完整性。而且整套方案只需要一个 npm 脚本就能串联执行,非常适合集成到现代 CI/CD 工作流中。至此,一个具备工程化测试保障的 Electron 项目就真正立住了。