Skip to main content

자동화 테스트

테스트 자동화는 애플리케이션 코드가 의도한 대로 동작하는지 검증하는 효율적인 방법이다. Electron은 자체적인 테스트 솔루션을 적극적으로 유지하지 않지만, 이 가이드에서는 Electron 앱에서 종단 간(end-to-end) 자동화 테스트를 실행할 수 있는 몇 가지 방법을 소개한다.

WebDriver 인터페이스 사용하기​

ChromeDriver - WebDriver for Chrome에서 발췌:

WebDriver는 다양한 브라우저에서 웹 애플리케이션을 자동으로 테스트하기 위한 오픈소스 도구다. 웹 페이지로 이동하거나 사용자 입력을 처리하고, 자바스크립트를 실행하는 등의 기능을 제공한다. ChromeDriver는 Chromium을 위한 WebDriver의 와이어 프로토콜을 구현한 독립 실행형 서버다. Chromium과 WebDriver 팀의 구성원들이 개발하고 있다.

WebDriver를 사용해 테스트를 설정하는 몇 가지 방법이 있다.

WebdriverIO 활용하기​

WebdriverIO (WDIO)는 WebDriver를 사용해 테스트를 수행할 수 있는 Node.js 패키지를 제공하는 테스트 자동화 프레임워크다. 이 프레임워크는 다양한 플러그인(예: 리포터와 서비스)도 포함하고 있어 테스트 환경을 구성하는 데 도움을 준다.

이미 WebdriverIO 설정이 있다면, 의존성을 업데이트하고 기존 설정을 공식 문서에 명시된 내용과 비교해 검증하는 것이 좋다.

테스트 러너 설치하기​

프로젝트에 WebdriverIO를 아직 사용하지 않는다면, 프로젝트 루트 디렉토리에서 스타터 툴킷을 실행해 추가할 수 있다:

npm init wdio@latest ./

이 명령어는 설정 마법사를 시작해 적절한 설정을 구성하고, 필요한 모든 패키지를 설치하며, wdio.conf.js 설정 파일을 생성한다. 설정 과정 중 _"어떤 타입의 테스트를 수행하고 싶은가요?"_라는 질문이 나오면 _"Electron 애플리케이션의 데스크톱 테스트"_를 선택해야 한다.

Electron 앱에 WDIO 연결하기​

설정 마법사를 실행한 후, wdio.conf.js 파일에는 대략 다음과 같은 내용이 포함된다:

wdio.conf.js
export const config = {
// ...
services: ['electron'],
capabilities: [{
browserName: 'electron',
'wdio:electronServiceOptions': {
// WebdriverIO는 Electron Forge나 electron-builder를 사용할 경우
// 자동으로 번들링된 애플리케이션을 찾을 수 있다. 그렇지 않다면
// 여기에서 직접 정의할 수 있다. 예를 들어:
// appBinaryPath: './path/to/bundled/application.exe',
appArgs: ['foo', 'bar=baz']
}
}]
// ...
}

테스트 작성하기​

WebdriverIO API를 사용해 화면의 엘리먼트와 상호작용할 수 있다. 이 프레임워크는 애플리케이션 상태를 쉽게 검증할 수 있는 커스텀 "matchers"를 제공한다. 예를 들어:

import { browser, $, expect } from '@wdio/globals'

describe('keyboard input', () => {
it('should detect keyboard input', async () => {
await browser.keys(['y', 'o'])
await expect($('keypress-count')).toHaveText('YO')
})
})

또한 WebdriverIO를 통해 Electron API에 접근해 애플리케이션의 정적 정보를 가져올 수 있다:

import { browser, $, expect } from '@wdio/globals'

describe('when the make smaller button is clicked', () => {
it('should decrease the window height and width by 10 pixels', async () => {
const boundsBefore = await browser.electron.browserWindow('getBounds')
expect(boundsBefore.width).toEqual(210)
expect(boundsBefore.height).toEqual(310)

await $('.make-smaller').click()
const boundsAfter = await browser.electron.browserWindow('getBounds')
expect(boundsAfter.width).toEqual(200)
expect(boundsAfter.height).toEqual(300)
})
})

다른 Electron 프로세스 정보를 가져오는 것도 가능하다:

import fs from 'node:fs'
import path from 'node:path'
import { browser, expect } from '@wdio/globals'

const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), { encoding: 'utf-8' }))
const { name, version } = packageJson

describe('electron APIs', () => {
it('should retrieve app metadata through the electron API', async () => {
const appName = await browser.electron.app('getName')
expect(appName).toEqual(name)
const appVersion = await browser.electron.app('getVersion')
expect(appVersion).toEqual(version)
})

it('should pass args through to the launched application', async () => {
// 커스텀 인자는 WDIO가 시작되기 전에 설정해야 하므로 wdio.conf.js 파일에서 설정됨
const argv = await browser.electron.mainProcess('argv')
expect(argv).toContain('--foo')
expect(argv).toContain('--bar=baz')
})
})

테스트를 실행하려면 다음 명령어를 사용한다:

$ npx wdio run wdio.conf.js

WebdriverIO는 애플리케이션을 자동으로 실행하고 종료하는 작업을 지원한다.

추가 문서​

Mocking Electron API와 다른 유용한 리소스에 대한 더 많은 문서는 공식 WebdriverIO 문서에서 확인할 수 있다.

Selenium 활용하기​

Selenium은 웹 자동화 프레임워크로, 다양한 언어에서 WebDriver API에 접근할 수 있는 바인딩을 제공한다. Node.js용 바인딩은 NPM의 selenium-webdriver 패키지에서 이용할 수 있다.

ChromeDriver 서버 실행하기​

Electron과 Selenium을 함께 사용하려면 electron-chromedriver 바이너리를 다운로드하고 실행해야 한다.

npm install --save-dev electron-chromedriver
./node_modules/.bin/chromedriver
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.

이때 사용된 포트 번호 9515는 나중에 사용되므로 기억해 두자.

Selenium과 ChromeDriver 연결하기​

먼저 프로젝트에 Selenium을 설치한다:

npm install --save-dev selenium-webdriver

selenium-webdriver를 Electron과 함께 사용하는 방법은 일반 웹사이트와 동일하다. 단, ChromeDriver에 연결하는 방법과 Electron 앱의 바이너리 위치를 직접 지정해야 한다:

test.js
const webdriver = require('selenium-webdriver')
const driver = new webdriver.Builder()
// "9515"는 ChromeDriver가 열어 놓은 포트다.
.usingServer('http://localhost:9515')
.withCapabilities({
'goog:chromeOptions': {
// 여기에 Electron 바이너리의 경로를 입력한다.
binary: '/Path-to-Your-App.app/Contents/MacOS/Electron'
}
})
.forBrowser('chrome') // 참고: selenium-webdriver <= 3.6.0 버전에서는 .forBrowser('electron')을 사용한다.
.build()
driver.get('https://www.google.com')
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver')
driver.findElement(webdriver.By.name('btnG')).click()
driver.wait(() => {
return driver.getTitle().then((title) => {
return title === 'webdriver - Google Search'
})
}, 1000)
driver.quit()

Playwright 사용하기​

Microsoft Playwright는 브라우저별 원격 디버깅 프로토콜을 사용해 구축된 엔드투엔드 테스트 프레임워크다. Puppeteer의 헤드리스 Node.js API와 유사하지만, 엔드투엔드 테스트에 더 초점을 맞춘다. Playwright는 Chrome DevTools Protocol (CDP)에 대한 Electron의 지원을 통해 실험적인 Electron 지원도 제공한다.

의존성 설치​

원하는 Node.js 패키지 매니저를 통해 Playwright를 설치한다. Playwright는 엔드 투 엔드 테스트를 위해 설계된 자체 테스트 러너를 제공한다:

npm install --save-dev @playwright/test
의존성 관련 주의사항

이 튜토리얼은 @playwright/test@1.41.1 버전을 기준으로 작성되었다. 아래 코드에 영향을 미칠 수 있는 변경 사항을 확인하려면 Playwright 릴리스 페이지를 참고한다.

테스트 작성하기​

Playwright는 _electron.launch API를 통해 개발 모드에서 앱을 실행한다. 이 API가 Electron 앱을 가리키도록 하려면 메인 프로세스의 진입점(여기서는 main.js) 경로를 전달하면 된다.

const { test, _electron: electron } = require('@playwright/test')

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
// 앱 종료
await electronApp.close()
})

이후에는 Playwright의 ElectronApp 클래스 인스턴스에 접근할 수 있다. 이 클래스는 메인 프로세스 모듈에 접근할 수 있는 강력한 기능을 제공한다. 예를 들어:

const { test, _electron: electron } = require('@playwright/test')

test('get isPackaged', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// 이 코드는 Electron의 메인 프로세스에서 실행되며, 여기서의 매개변수는 항상
// 메인 앱 스크립트에서 require('electron')의 결과다.
return app.isPackaged
})
console.log(isPackaged) // false (개발 모드이기 때문)
// 앱 종료
await electronApp.close()
})

또한 이 클래스는 Electron BrowserWindow 인스턴스에서 개별 Page 객체를 생성할 수 있다. 예를 들어, 첫 번째 BrowserWindow를 가져와 스크린샷을 저장하려면:

const { test, _electron: electron } = require('@playwright/test')

test('save screenshot', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })
// 앱 종료
await electronApp.close()
})

이 모든 것을 Playwright 테스트 러너를 사용해 결합해 보자. example.spec.js 테스트 파일을 만들고 단일 테스트와 단언을 추가한다:

example.spec.js
const { test, expect, _electron: electron } = require('@playwright/test')

test('example test', async () => {
const electronApp = await electron.launch({ args: ['.'] })
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// 이 코드는 Electron의 메인 프로세스에서 실행되며, 여기서의 매개변수는 항상
// 메인 앱 스크립트에서 require('electron')의 결과다.
return app.isPackaged
})

expect(isPackaged).toBe(false)

// 첫 번째 BrowserWindow가 열릴 때까지 기다린 후
// 해당 Page 객체를 반환
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })

// 앱 종료
await electronApp.close()
})

그런 다음 npx playwright test를 실행해 Playwright Test를 실행한다. 콘솔에서 테스트가 통과되는 것을 확인할 수 있으며, 파일 시스템에 intro.png 스크린샷이 생성된다.

☁  $ npx playwright test

Running 1 test using 1 worker

✓ example.spec.js:4:1 › example test (1s)
info

Playwright Test는 .*(test|spec)\.(js|ts|mjs) 정규식과 일치하는 파일을 자동으로 실행한다. 이 매칭은 Playwright Test 설정 옵션에서 커스터마이즈할 수 있다. 또한 TypeScript도 기본적으로 지원한다.

추가 자료

Playwright의 Electron 및 ElectronApplication 클래스 API에 대한 전체 문서를 확인해 보자.

커스텀 테스트 드라이버 사용하기​

Node.js의 내장 IPC-over-STDIO를 활용해 직접 테스트 드라이버를 작성할 수도 있다. 커스텀 테스트 드라이버는 추가적인 앱 코드를 작성해야 하지만, 오버헤드가 적고 테스트 스위트에 커스텀 메서드를 노출할 수 있다는 장점이 있다.

커스텀 드라이버를 만들기 위해 Node.js의 child_process API를 사용한다. 테스트 스위트는 Electron 프로세스를 실행한 후 간단한 메시징 프로토콜을 설정한다:

testDriver.js
const childProcess = require('node:child_process')
const electronPath = require('electron')

// 프로세스 실행
const env = { /* ... */ }
const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })

// 앱에서 보낸 IPC 메시지 수신
appProcess.on('message', (msg) => {
// ...
})

// 앱에 IPC 메시지 전송
appProcess.send({ my: 'message' })

Electron 앱 내부에서는 Node.js process API를 사용해 메시지를 수신하고 응답을 보낼 수 있다:

main.js
// 테스트 스위트에서 보낸 메시지 수신
process.on('message', (msg) => {
// ...
})

// 테스트 스위트에 메시지 전송
process.send({ my: 'message' })

이제 appProcess 객체를 사용해 테스트 스위트와 Electron 앱 간에 통신할 수 있다.

편의를 위해 appProcess를 더 높은 수준의 함수를 제공하는 드라이버 객체로 감싸는 것이 좋다. 이를 위해 TestDriver 클래스를 만들어 보자:

testDriver.js
class TestDriver {
constructor ({ path, args, env }) {
this.rpcCalls = []

// 자식 프로세스 시작
env.APP_TEST_DRIVER = 1 // 앱이 메시지를 수신해야 함을 알림
this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })

// RPC 응답 처리
this.process.on('message', (message) => {
// 핸들러 추출
const rpcCall = this.rpcCalls[message.msgId]
if (!rpcCall) return
this.rpcCalls[message.msgId] = null
// reject/resolve 처리
if (message.reject) rpcCall.reject(message.reject)
else rpcCall.resolve(message.resolve)
})

// 준비 완료 대기
this.isReady = this.rpc('isReady').catch((err) => {
console.error('애플리케이션 시작 실패', err)
this.stop()
process.exit(1)
})
}

// 간단한 RPC 호출
// 사용법: driver.rpc('method', 1, 2, 3).then(...)
async rpc (cmd, ...args) {
// RPC 요청 전송
const msgId = this.rpcCalls.length
this.process.send({ msgId, cmd, args })
return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject }))
}

stop () {
this.process.kill()
}
}

module.exports = { TestDriver }

앱 코드에서는 RPC 호출을 수신하는 간단한 핸들러를 작성할 수 있다:

main.js
const METHODS = {
isReady () {
// 필요한 설정 수행
return true
}
// RPC 호출 가능한 메서드 정의
}

const onMessage = async ({ msgId, cmd, args }) => {
let method = METHODS[cmd]
if (!method) method = () => new Error('잘못된 메서드: ' + cmd)
try {
const resolve = await method(...args)
process.send({ msgId, resolve })
} catch (err) {
const reject = {
message: err.message,
stack: err.stack,
name: err.name
}
process.send({ msgId, reject })
}
}

if (process.env.APP_TEST_DRIVER) {
process.on('message', onMessage)
}

그런 다음 테스트 스위트에서 원하는 테스트 자동화 프레임워크와 함께 TestDriver 클래스를 사용할 수 있다. 다음 예제는 ava를 사용하지만, Jest나 Mocha와 같은 다른 인기 있는 선택지도 가능하다:

test.js
const test = require('ava')
const electronPath = require('electron')
const { TestDriver } = require('./testDriver')

const app = new TestDriver({
path: electronPath,
args: ['./app'],
env: {
NODE_ENV: 'test'
}
})
test.before(async t => {
await app.isReady
})
test.after.always('cleanup', async t => {
await app.stop()
})