테스트 — Spectron, Playwright로 E2E 테스트
"Electron 앱도 테스트 없이는 리팩토링이 두렵다" — 메인 프로세스 로직, 렌더러 UI, IPC 통합까지 계층별 테스트 전략이 필요합니다.
테스트 계층
┌─────────────────────────────┐
│ E2E 테스트 │ ← Playwright for Electron
│ (실제 앱 실행) │
├─────────────────────────────┤
│ 통합 테스트 │ ← IPC 핸들러 + 모듈 연동
├─────────────────────────────┤
│ 유닛 테스트 │ ← 순수 함수, 유틸리티
│ (Vitest / Jest) │
└─────────────────────────────┘
유닛 테스트 (Vitest)
npm install vitest --save-dev
// utils/parser.js
function parseDeepLink(url) {
try {
const parsed = new URL(url);
return {
action: parsed.hostname,
params: Object.fromEntries(parsed.searchParams),
};
} catch {
return null;
}
}
module.exports = { parseDeepLink };
// utils/parser.test.js
import { describe, it, expect } from 'vitest';
import { parseDeepLink } from './parser';
describe('parseDeepLink', () => {
it('올바른 딥링크를 파싱한다', () => {
const result = parseDeepLink('myapp://open?file=test.txt');
expect(result).toEqual({
action: 'open',
params: { file: 'test.txt' },
});
});
it('잘못된 URL은 null을 반환한다', () => {
expect(parseDeepLink('not-a-url')).toBeNull();
});
});
E2E 테스트 (Playwright)
npm install @playwright/test electron --save-dev
// e2e/app.spec.js
const { test, expect, _electron: electron } = require('@playwright/test');
let electronApp;
let page;
test.beforeAll(async () => {
electronApp = await electron.launch({
args: ['.'],
});
page = await electronApp.firstWindow();
await page.waitForLoadState('domcontentloaded');
});
test.afterAll(async () => {
await electronApp.close();
});
test('앱이 정상적으로 실행된다', async () => {
const title = await page.title();
expect(title).toBe('내 앱');
});
test('버전 정보가 표시된다', async () => {
const version = await page.locator('#version').textContent();
expect(version).toMatch(/\d+\.\d+\.\d+/);
});
test('파일 열기 다이얼로그가 동작한다', async () => {
// IPC 호출을 모킹
await electronApp.evaluate(async ({ dialog }) => {
dialog.showOpenDialog = async () => ({
canceled: false,
filePaths: ['/tmp/test.txt'],
});
});
await page.click('#open-file-btn');
const filePath = await page.locator('#current-file').textContent();
expect(filePath).toContain('test.txt');
});
test('창 최소화/최대화가 동작한다', async () => {
const win = await electronApp.firstWindow();
// 최대화
await electronApp.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].maximize();
});
const isMaximized = await electronApp.evaluate(({ BrowserWindow }) => {
return BrowserWindow.getAllWindows()[0].isMaximized();
});
expect(isMaximized).toBe(true);
});
IPC 핸들러 테스트
// 메인 프로세스 로직을 분리하여 테스트 가능하게
// handlers/notes.js
class NoteHandlers {
constructor(db) {
this.db = db;
}
async getAll() {
return this.db.prepare('SELECT * FROM notes').all();
}
async create(title, content) {
const result = this.db.prepare(
'INSERT INTO notes (title, content) VALUES (?, ?)'
).run(title, content);
return { id: result.lastInsertRowid, title, content };
}
}
module.exports = { NoteHandlers };
// handlers/notes.test.js
import { describe, it, expect, beforeEach } from 'vitest';
import Database from 'better-sqlite3';
import { NoteHandlers } from './notes';
describe('NoteHandlers', () => {
let db;
let handlers;
beforeEach(() => {
db = new Database(':memory:');
db.exec('CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT, content TEXT)');
handlers = new NoteHandlers(db);
});
it('노트를 생성할 수 있다', async () => {
const note = await handlers.create('테스트', '내용');
expect(note.title).toBe('테스트');
expect(note.id).toBeDefined();
});
it('모든 노트를 조회할 수 있다', async () => {
await handlers.create('노트1', '내용1');
await handlers.create('노트2', '내용2');
const notes = await handlers.getAll();
expect(notes).toHaveLength(2);
});
});
면접 포인트 정리
- 유닛 테스트: 순수 함수/유틸리티를 Vitest로 검증
- E2E 테스트: Playwright의 Electron 지원으로 실제 앱 실행 후 검증
- IPC 핸들러 로직을 분리하면 유닛 테스트가 가능해짐
- Electron 모듈 의존성이 있는 코드는 모킹이나 E2E로 테스트
테스트를 설정했으면, 다음은 오프스크린 렌더링을 알아봅시다.
댓글 로딩 중...