팀원분들과 함께 프로젝트를 진행하면서, 마지막으로 E2E 테스트 과제가 남아있었다.
E2E 테스트는 이전에 한번도 해본 적이 없어서 멘토님께 cypress에 대해서 여쭤보니, playwright라는 라이브러리를 알려주셔서 사용해보게 되었다!
등에 대한 블로그 글들은 이미 많이 있었지만,
간단 사용 가이드를 정리한 글은 잘 없기에 React 프로젝트에 적용한 경험을 이번 기회에 정리해보려고 한다!
E2E 테스트가 낮설고 경험이 없어도 가이드와 함께 설정 및 실행, 그리고 테스트 코드를 작성할 수 있도록 해보자!

playwright는 MS에서 제작한 오픈소스 웹 테스트 자동화 라이브러리 입니다!
하나의 API로 여러 브라우저를 테스트할 수 있고, codegen 및 UI 명령어를 통해 간편하게 테스트 코드 작성 및 시각적으로 테스트 결과를 확인할 수 있습니다.
또한 github action과 연계하여 특정 브랜치에 PR 혹은 Merge 시 자동으로 테스트를 동작시킬 수 있습니다!
더 자세한 내용은 ---> playwright 공식 문서!
그럼 우선 vite를 통해 바로 react 프로젝트를 세팅해 봅시다.
이번에는 github action을 이용해 CI(지속적 통합, Continous Intergration) 과정에서 자동으로 E2E 테스트를 동작시켜 보는 시나리오를 가정하기 때문에 github repo도 하나 만들어 줍시다!
npm create vite@latest <projectName> -- --template react

git init
git remote add origin <githup repo url>

react 프로젝트를 세팅했다면, 다음으로 playwright를 설치해줍니다!
npm init playwright@latest

설치하고 나면 추가된 파일들에서 에러가 날 수 있습니다.
vite 최신 버전의 경우 ES module 방식을 채택하지만
playwright의 JavaScript 타입을 설치하면 common JS로 파일이 작성되기 때문에 충돌이 발생하므로 아래와 같이 두 파일을 수정해줍니다.
(TypeScript로 설치하면 ES module방식으로 파일이 작성됩니다.)
// tests/example.spec.ts
// 예시 테스트 파일입니다.
// @ts-check
// const { test, expect } = require('@playwright/test'); -> Common js
import { test, expect } from '@playwright/test' // -> ES module
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
// playwright.config.js
// playwright 설정 파일입니다.
// @ts-check
// const { defineConfig, devices } = require('@playwright/test');
import { defineConfig, devices } from "@playwright/test"
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
// 테스트 파일이 위치할 폴더명 입니다.
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
// 테스트 시 실행할 브라우저 설정입니다. 간단하게 chrome만 작성합니다.
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
/* Run your local dev server before starting the tests */
// webServer 설정은 vite의 npm run dev 명령어와 동일하게 작성합니다.
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
webServer 설정의 경우 npx playwright test 명령어를 실행할 시 playwright가 테스트를 실행할 웹 서버를 실행할 수 있도록 해주는 설정 입니다.
따라서 vite의 로컬 테스트 명령어를 작성하고 url 또한 동일하게 맞춰줍니다.
npx playwright test 명령어는 추후 github action에서 동일하게 사용됩니다.
마지막으로 .eslint.cjs 파일의 env 설정에 node를 추가해 playwright.config.js 파일 내 process를 인식할 수 있도록 수정해줍니다.
// .eslint.cjs
module.exports = {
root: true,
env: { browser: true, es2020: true, node: true }, // node: true 추가!
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
// example.spec.ts
// @ts-check
// const { test, expect } = require('@playwright/test');
import { test, expect } from '@playwright/test'
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
방금 전에 수정한 테스트 파일 입니다.
test, expect를 import 하고 있는데, 두 함수의 역할은 다음과 같습니다.
즉 위의 두 함수를 이용해 사용자의 동작을 실행시키고 결과를 확인해
playwright를 이용한 E2E 테스트를 진행할 수 있습니다.
playwright를 실행시키기 위한 리액트 프로젝트 및 세팅이 끝났으므로, 처음 playwright를 설치할 때 추가된 tests/example.spec.js 테스트를 실행시켜 봅시다!
npx playwright test // 이 명령어는 모든 테스트 파일을 수행합니다.
npx playwright test example.spec.js // 해당 테스트 파일만 수행합니다.

playwright는 실행 결과를 html 파일로 만들어 줍니다.
다음 코드를 실행해 결과를 브라우저로 확인할 수 있습니다.
npx playwright show-report

playwright는 ui 모드 또한 지원합니다.
npx playwright test --ui // 작성한 모든 테스트를 ui 모드로 확인할 수 있습니다.

위와 같이 작성한 테스트의 순서대로 화면의 변화를 확인할 수 있습니다!
다만 주의할 점은, ui 모드는 테스트 단계별 화면 및 가장 마지막으로 수행한 테스트로 인해 변경된 상태, 혹은 화면만 보여줍니다.
즉 마지막 테스트 코드 이후로 발생한 추가적인 비동기 통신으로 인해 화면이 변경되더라도 추가적인 상호작용을 발생시키는 코드가 없다면 변경된 화면을 보여주지 않습니다.
예를 들어 아래 테스트의 경우 페이지 이동 이후 추가적인 상호작용(사용자 동작)이 없기 때문에 API 통신중일때 나오는 스켈레톤 디자인이 계속 나오게 됩니다.

codegen 기능을 이용하면, 굉장히 쉽게 테스트 코드를 작성할 수 있습니다.
npx playwright codegen

위 사진과 같이 codegen 명령어 수행시 시크릿 브라우저 및 playwright inspector가 팝업됩니다. 브라우저에서 수행되는 모든 동작은 테스트 코드로 변환되며, 이 덕분에 매우 손쉽게 테스트 코드를 작성할 수 있습니다.
또한 record를 클릭해 기록 여부를 변경할 수 있으며 locator를 이용해 CSS 선택자와 같이 화면 내 요소를 가져올 수 있습니다.
한가지 유의할 점은 codegen의 경우 webserver를 따로 실행하지 않으므로 로컬 서버를 테스트 하고 싶은 경우 codegen 명령어 실행 전 추가로 터미널을 하나 더 켠 뒤 로컬서버를 실행시켜야 합니다.
// 첫번째 터미널
npm run dev
// 두번째 터미널
npx playwright codegen
그럼 지금까지 배운 지식을 토대로, 처음에 생성한 리액트 프로젝트의 테스트 코드를 작성해 봅시다!
로컬 서버를 실행시킨 후, codegen 명령어를 입력합니다.
이후 바로 나타난 브라우저에서 vite의 default port로 이동합니다.
// 첫번째 터미널
npm run dev
// 두번째 터미널
npx playwright codegen
이후 count 버튼을 클릭하면, 옆과 같이 자동으로 코드를 작성해 줍니다.
또한 녹화를 멈추고 locator 버튼을 이용해 페이지 내 버튼을 가져올 수 있습니다.

그러면 한번 클릭했을 때 count의 값은 1이므로, 해당 값을 판별하는 것으로 테스트를 작성해 봅시다.
테스트는 playwright.config.js의 testDir에 명시한 폴더 내에 작성해야 합니다.
// tests/main.spec.js
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('http://localhost:5173/');
await page.getByRole('button', { name: 'count is' }).click();
const button = page.getByRole('button', { name: 'count is' })
await expect(button).toContainText("count is 1")
});

ui 모드로 확인하면 입력한 행동에 맞춰 화면이 바뀌는것을 확인하실 수 있습니다. 또한 어떤 요소를 선택했는지도 화면에 표시됩니다.

자, 이제 테스트 코드도 작성했으니 마지막으로 main 브랜치에 merge 및 PR시 github action을 통해 E2E 테스트가 동작하는지 확인해 봅시다!
기본적으로 작성되있는 .github/workflows/playwright.yml 파일에는
main 혹은 master 브랜치에 merge 혹은 PR 생성시 E2E 테스트가 동작되도록 설정되어 있습니다.
github action 및 yml 파일에 대한 내용은 이번 포스팅에선 다루지 않겠습니다!
git add .
git commit -m "feat : 프로젝트 세팅 및 테스트 코드 작성"


성공적으로 테스트 완료!
이제 react 와 playwright 세팅을 통해 언제든지 E2E 테스트를 작성할 수 있는 능력을 가지게 되었습니다!
Common JS와 ES module에 대해 조금 찾아보신다면 CRA든 vite든, TS JS 상관없이 적용하실 수 있을겁니다!
기술을 도입할 때 해당 기술이 왜 필요한지, 다른 기술들과 비교하여 어떤것을 선택해야 할지 의논 끝에 고르는 것도 개발에 있어서 매우 중요합니다.
하지만 이렇게 무작정 시도해 보고, 더 세부적인 내용들은 직접 사용해 보면서 깨우쳐 나가는것도 성장에 있어서 중요하다고 생각됩니다.
E2E테스트에 대해 잘 몰라도, 처음 도입을 시도해 보려고 하시는 분들에게 큰 도움이 되기를 바랍니다!

잘정리하셨네요~ ! 화이팅입니다~