출시·고도화 중
Electron 안내서 · 5/6
Electron 앱은 메인 프로세스, 프리로드, 렌더러가 함께 움직이기 때문에 한 가지 테스트로 모든 것을 확인하기 어렵습니다. 이 장에서는 Electron에 의존하지 않는 로직을 단위 테스트로 검사하고, 실제 앱을 띄워 창을 조작하는 엔드 투 엔드(E2E) 테스트를 Playwright로 작성하는 방법을 다룹니다.
| 종류 | 대상 | 도구 |
|---|---|---|
| 단위 테스트 | 순수 함수, 데이터 변환, 검증 로직 | Vitest, Jest |
| 모듈 테스트 | Electron API를 쓰는 메인 프로세스 코드 | Vitest + electron 모의 객체 |
| 렌더러 컴포넌트 테스트 | React · Vue 컴포넌트 | 각 프레임워크의 테스트 도구 |
| E2E 테스트 | 실행한 앱 전체 | Playwright, WebdriverIO |
테스트하기 쉬운 앱을 만드는 첫걸음은 Electron API와 업무 로직을 분리하는 것입니다. IPC 처리기 안에 로직을 길게 쓰지 말고, 순수 함수로 빼낸 뒤 처리기에서는 그 함수를 부르기만 합니다.
최근 파일 목록을 관리하는 함수를 예로 들어 보겠습니다. 이 함수는 Electron을 전혀 모르므로 일반 Node.js 테스트로 검사할 수 있습니다.
// src/lib/recent-files.js
export function addRecentFile(list, file, max = 10) {
return [file, ...list.filter((item) => item !== file)].slice(0, max)
}npm install --save-dev vitest
npx vitest// tests/recent-files.test.js
import { describe, it, expect } from 'vitest'
import { addRecentFile } from '../src/lib/recent-files'
describe('addRecentFile', () => {
it('새 파일을 맨 앞에 넣는다', () => {
expect(addRecentFile(['a.txt'], 'b.txt')).toEqual(['b.txt', 'a.txt'])
})
it('이미 있는 파일은 맨 앞으로 옮긴다', () => {
expect(addRecentFile(['a.txt', 'b.txt'], 'b.txt')).toEqual(['b.txt', 'a.txt'])
})
it('최대 개수를 넘지 않는다', () => {
expect(addRecentFile(['a', 'b', 'c'], 'd', 3)).toEqual(['d', 'a', 'b'])
})
})일반 Node.js에서 require('electron')을 하면 API 객체가 아니라 Electron 실행 파일의 경로 문자열이 돌아옵니다. 그래서 메인 프로세스 코드를 Node.js 테스트에서 불러오려면 electron 모듈을 모의 객체로 바꿔야 합니다.
// src/main/settings-path.js
import { app } from 'electron'
import path from 'node:path'
export const settingsPath = () => path.join(app.getPath('userData'), 'settings.json')// tests/settings-path.test.ts
import { vi, it, expect } from 'vitest'
import path from 'node:path'
vi.mock('electron', () => ({
app: { getPath: vi.fn(() => '/tmp/test-user-data') },
}))
import { settingsPath } from '../src/main/settings-path'
it('userData 아래의 settings.json 을 가리킨다', () => {
expect(settingsPath()).toBe(path.join('/tmp/test-user-data', 'settings.json'))
})vi.mock은 파일 맨 위로 끌어올려 실행되므로, 아래의 import보다 먼저 적용됩니다. 다만 Vitest의 모의는 Vitest가 변환해 불러오는 모듈에 적용되고, Node.js가 require로 직접 불러오는 CommonJS 모듈에는 적용되지 않습니다. 그래서 위 예제의 메인 프로세스 코드는 ES 모듈 문법으로 썼습니다. Forge의 Vite · webpack 템플릿처럼 메인 프로세스를 번들러로 빌드하는 프로젝트라면 이렇게 쓰는 것이 자연스럽습니다.
Playwright는 Electron 앱을 직접 실행해 제어하는 기능을 실험적(experimental) 지원으로 제공합니다. _electron.launch()로 앱을 띄우면 ElectronApplication 객체를 받고, 여기서 창(Page)을 얻어 일반 웹 테스트처럼 조작합니다. evaluate()로는 메인 프로세스 안에서 코드를 실행할 수 있습니다.
npm install --save-dev @playwright/test
npx playwright test// tests/app.spec.ts
import { test, expect, _electron as electron } from '@playwright/test'
test('메인 창이 열린다', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
// 메인 프로세스에서 실행된다
const isPackaged = await electronApp.evaluate(async ({ app }) => app.isPackaged)
expect(isPackaged).toBe(false)
const window = await electronApp.firstWindow()
await expect(window).toHaveTitle('Hello Electron')
await expect(window.locator('h1')).toHaveText('Hello from Electron')
await electronApp.close()
})운영체제 대화 상자는 자동화할 수 없으므로, 메인 프로세스의 dialog 함수를 테스트 안에서 바꿔 끼웁니다.
test('파일 열기', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
await electronApp.evaluate(({ dialog }, filePath) => {
dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [filePath] })
}, '/tmp/sample.txt')
const window = await electronApp.firstWindow()
await window.getByRole('button', { name: 'Open' }).click()
await expect(window.locator('#file')).toHaveText('/tmp/sample.txt')
await electronApp.close()
})패키징한 앱을 검사하려면 electron.launch({ executablePath: '...' })에 빌드된 실행 파일 경로를 넘깁니다.
Linux CI에는 화면이 없으므로 가상 디스플레이를 써야 Electron 창을 띄울 수 있습니다.
# Ubuntu 예
sudo apt-get install -y xvfb
xvfb-run --auto-servernum npx playwright testWebdriverIO도 Electron 서비스(wdio-electron-service)로 E2E 테스트를 지원합니다. 예전에 쓰이던 Spectron은 더 이상 유지되지 않으므로 새 프로젝트에서는 쓰지 않습니다.
electron 모듈을 모의 객체로 바꿉니다._electron.launch()로 실제 앱을 띄워 창을 조작하고, evaluate()로 메인 프로세스를 검사합니다.xvfb-run으로 가상 디스플레이를 제공합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.