테스트 모범 사례
테스트 모범 사례 (Best Practices)
E2E 테스트가 "일단 돌아가는" 수준에 머물면 조금만 변화가 생겨도 금방 깨지기 마련이에요. Playwright가 권장하는 모범 사례를 따르면 테스트가 훨씬 탄탄해져요. 여기서 다루는 핵심은 사용자에게 보이는 동작을 테스트하고, 테스트끼리 완전히 격리하며, DOM 구조보다는 로케이터로 요소를 찾는 방식이에요. 이 원칙들이 실제로 어떻게 적용되는지 하나씩 살펴볼게요.
테스트 철학
사용자에게 보이는 동작을 테스트하기
자동화 테스트는 최종 사용자에게 앱이 제대로 동작하는지를 검증해야 해요. 함수 이름, 배열 여부, 특정 요소의 CSS 클래스 같은 구현 세부 사항에 의존하면 안 돼요 — 사용자는 그런 것들을 보지도 알지도 못하니까요. 사용자가 보는 렌더링된 출력과 상호작용하듯, 테스트도 같은 출력만 보도록 작성하는 게 좋아요.
테스트를 최대한 격리하기
각 테스트는 다른 테스트와 완전히 분리되어, 자기만의 로컬 스토리지·세션 스토리지·데이터·쿠키 등을 가지고 독립적으로 실행돼야 해요. 테스트 격리는 재현성을 높이고 디버깅을 쉽게 만들며, 테스트 실패가 연쇄적으로 퍼지는 걸 막아 줘요.
테스트 중 일부를 반복하지 않으려면 before·after 훅을 쓰면 돼요. 예를 들어 각 테스트 전에 특정 URL로 이동하거나 앱에 로그인하는 코드를 before 훅에 넣어 두면, 어떤 테스트도 다른 테스트에 의존하지 않게 돼요.
import { test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
// Runs before each test and signs in each page.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('username');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
});
test('first', async ({ page }) => {
// page is signed in.
});
test('second', async ({ page }) => {
// page is signed in.
});
테스트가 단순하다면 약간의 중복은 오히려 코드를 더 명확하고 읽기·유지보수하기 쉽게 만들 수 있으니 그 정도는 괜찮아요. 로그인 상태를 여러 테스트에서 재사용하려면 setup 프로젝트를 써서 한 번만 로그인하고 이후 테스트에서는 로그인 단계를 건너뛸 수도 있어요.
서드파티 의존성 테스트 피하기
내가 통제할 수 있는 것만 테스트해야 해요. 외부 사이트나 통제할 수 없는 서드파티 서버로의 링크를 테스트하려 하지 마세요. 시간이 들고 테스트가 느려질 뿐 아니라, 링크하는 페이지의 콘텐츠나 쿠키 배너·오버레이 같은 것들을 통제할 수 없어 테스트가 실패할 수 있어요.
대신 Playwright의 네트워크 API로 필요한 응답을 보장해 줘요.
await page.route('**/api/fetch_data_third_party_dependency', route => route.fulfill({
status: 200,
body: testData,
}));
await page.goto('https://example.com');
데이터베이스로 테스트하기
데이터베이스로 작업한다면 데이터를 직접 통제해야 해요. 스테이징 환경에서 테스트하고 그 데이터가 변하지 않도록 해요. 시각적 회귀 테스트라면 운영체제와 브라우저 버전이 동일한지도 확인해야 해요.
모범 사례
로케이터 사용하기
E2E 테스트를 작성하려면 먼저 웹페이지에서 요소를 찾아야 해요. Playwright의 내장 로케이터를 쓰면 되는데, 로케이터는 자동 대기(auto waiting)와 재시도 가능성(retry-ability)을 갖고 있어요. 자동 대기는 클릭을 수행하기 전에 요소가 보이는지·활성화됐는지 같은 actionability 검사를 Playwright가 수행한다는 뜻이에요. 탄탄한 테스트를 위해 사용자에게 보이는 속성과 명시적인 계약을 우선하는 게 좋아요.
// 👍
page.getByRole('button', { name: 'submit' });
체이닝과 필터링 사용하기
로케이터는 체이닝으로 페이지의 특정 부분으로 검색 범위를 좁힐 수 있어요. 텍스트나 다른 로케이터로 로케이터를 필터링할 수도 있고요.
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await page
.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button', { name: 'Add to cart' })
.click();
XPath·CSS 선택자보다 사용자 지향 속성 우선하기
DOM은 쉽게 바뀌기 때문에 테스트가 DOM 구조에 의존하면 실패할 가능성이 커져요. 예를 들어 버튼을 CSS 클래스로 선택했다고 해 볼게요 — 디자이너가 클래스를 바꾸면 테스트가 깨져 버려요.
// 👎
page.locator('button.buttonIcon.episode-actions-later');
DOM 변화에 강한 로케이터를 쓰는 게 좋아요.
// 👍
page.getByRole('button', { name: 'submit' });
로케이터 생성하기
Playwright의 테스트 생성기는 테스트를 만들고 로케이터를 골라 주는데, 페이지를 보면서 role·text·test id 로케이터를 우선한 최선의 로케이터를 찾아 줘요. 요소가 여러 개 매칭되면 로케이터를 개선해 유일하게 식별되도록 하니, 로케이터 때문에 테스트가 깨질 걱정은 하지 않아도 돼요.
codegen으로 로케이터 생성
로케이터를 고르려면 codegen 명령에 로케이터를 고를 URL을 붙여 실행해요.
npx playwright codegen playwright.dev
yarn playwright codegen playwright.dev
pnpm exec playwright codegen playwright.dev
그러면 새 브라우저 창과 Playwright Inspector가 열려요. 먼저 'Record' 버튼을 눌러 녹화를 중지하면 'Pick Locator' 버튼이 활성화돼요. 브라우저 요소 위에 마우스를 올리면 커서 아래에 로케이터가 강조되고, 요소를 클릭하면 로케이터가 Playwright Inspector에 추가돼요. 그대로 복사해 테스트 파일에 붙여넣거나, 텍스트를 수정해 브라우저에서 결과를 확인하는 식으로 로케이터를 다듬을 수도 있어요. VS Code 확장으로도 로케이터를 생성하고 테스트를 기록할 수 있어요.
Web-first 단언 사용하기
단언은 기대 결과와 실제 결과가 일치하는지 검증하는 방법이에요. Web-first 단언을 쓰면 Playwright가 기대 조건이 충족될 때까지 기다려요. 예를 들어 경고 메시지 테스트에서 메시지가 나타나게 하는 버튼을 클릭했는데, 메시지가 나타나는 데 0.5초가 걸린다면 toBeVisible() 같은 단언이 필요하면 재시도하며 기다려요.
// 👍
await expect(page.getByText('welcome')).toBeVisible();
// 👎
expect(await page.getByText('welcome').isVisible()).toBe(true);
수동 단언 쓰지 않기
expect 앞이 아니라 안에 await가 들어간 수동 단언은 쓰지 마세요. isVisible() 같은 단언은 기다려 주지 않고 로케이터가 존재하는지만 확인하고 즉시 반환해요 — 1초도 기다리지 않아요. toBeVisible() 같은 Web-first 단언을 쓰는 게 맞아요.
// 👎
expect(await page.getByText('welcome').isVisible()).toBe(true);
// 👍
await expect(page.getByText('welcome')).toBeVisible();
디버깅 구성하기
로컬 디버깅은 VS Code 확장을 설치해 VS Code에서 라이브로 디버그하는 걸 권장해요. 테스트 옆 줄을 우클릭해 디버그 모드로 실행하면 브라우저가 열리고 중단점에서 멈춰요. Playwright Inspector로 디버그하려면 --debug 플래그로 테스트를 실행하고요. 특정 테스트만 디버그하려면 테스트 파일 이름과 줄 번호를 붙이고 --debug를 달아요.
npx playwright test example.spec.ts:9 --debug
CI에서 디버깅
CI 실패를 분석할 땐 비디오·스크린샷 대신 트레이스 뷰어를 쓰는 걸 권장해요. 트레이스 뷰어는 테스트의 전체 트레이스를 로컬 PWA(Progressive Web App)로 제공해 쉽게 공유할 수 있어요. 타임라인을 보고, 각 액션의 DOM 스냅샷을 dev tools로 검사하고, 네트워크 요청을 볼 수 있어요.
트레이스는 Playwright 설정 파일에서 구성하며, CI에서는 실패 테스트의 첫 재시도에서 실행되도록 설정돼 있어요. on으로 설정해 매 테스트마다 트레이스를 남기는 건 성능 부담이 크니 권장하지 않아요. 개발 중에는 --trace 플래그로 로컬에서 트레이스를 실행할 수 있어요.
npx playwright test --trace on
실행 후 각 테스트의 트레이스가 HTML 리포트에서 바로 볼 수 있어요.
npx playwright show-report
Playwright 툴링 사용하기
Playwright는 테스트 작성을 돕는 다양한 툴을 갖고 있어요.
- VS Code 확장 — 작성·실행·디버깅에서 좋은 개발자 경험 제공
- 테스트 생성기 — 테스트 생성·로케이터 선택
- 트레이스 뷰어 — 타임라인·DOM 스냅샷·네트워크 요청을 볼 수 있는 전체 트레이스 제공
- UI Mode — time travel 경험과 watch 모드로 테스트 탐색·실행·디버깅
- TypeScript — 별도 설정 없이 동작하며 IDE 통합이 좋아요. TypeScript 경험이 없어도 되고,
test파일을.ts확장자로 만들기만 하면 돼요.
모든 브라우저에서 테스트하기
Playwright는 플랫폼과 무관하게 모든 브라우저에서 사이트를 테스트하기 쉽게 해 줘요. 설정 파일에서 프로젝트를 추가해 어떤 브라우저나 기기를 쓸지 정하면 돼요.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Playwright 의존성 최신 유지
Playwright 버전을 최신으로 유지하면 최신 브라우저 버전으로 테스트할 수 있고, 최신 브라우저가 공개되기 전에 문제를 잡을 수 있어요.
npm install -D @playwright/test@latest
yarn add --dev @playwright/test@latest
pnpm install --save-dev @playwright/test@latest
현재 버전 확인은 이렇게 해요.
npx playwright --version
CI에서 테스트 실행하기
CI/CD를 구성해 테스트를 자주 실행하세요. 가능하면 매 커밋과 풀 리퀘스트마다 실행하는 게 이상적이에요. Playwright는 별도 설정 없이도 CI에서 테스트를 돌려 주는 GitHub Actions 워크플로우를 제공해요.
CI에서는 Linux를 쓰는 게 더 저렴해요. 개발자는 로컬에서 아무 환경이나 써도 되지만 CI는 Linux로 구성하고, CI를 더 빠르게 하려면 샤딩을 고려해 보세요.
CI에서 브라우저 다운로드 최적화
특히 CI에서는 실제로 필요한 브라우저만 설치해야 해요. 예를 들어 Chromium으로만 테스트한다면 Chromium만 설치하면 돼요.
# Instead of installing all browsers
npx playwright install --with-deps
# Install only Chromium
npx playwright install chromium --with-deps
이러면 CI 머신의 다운로드 시간과 디스크 공간을 모두 아낄 수 있어요.
테스트 린트
테스트에는 TypeScript와 ESLint 린팅을 권장해요. @typescript-eslint/no-floating-promises ESLint 규칙으로 Playwright API 비동기 호출 앞에 빠진 await를 잡을 수 있고, CI에서 tsc --noEmit으로 함수가 올바른 시그니처로 호출되는지 확인할 수 있어요.
병렬성과 샤딩
Playwright는 기본적으로 테스트를 병렬로 실행해요. 한 파일의 테스트는 같은 워커 프로세스 안에서 순서대로 실행되고, 한 파일에 독립적인 테스트가 많다면 병렬로 실행하도록 설정할 수 있어요.
import { test } from '@playwright/test';
test.describe.configure({ mode: 'parallel' });
test('runs in parallel 1', async ({ page }) => { /* ... */ });
test('runs in parallel 2', async ({ page }) => { /* ... */ });
Playwright는 테스트 스위트를 샤드로 나눠 여러 머신에서 실행할 수도 있어요.
npx playwright test --shard=1/3
생산성 팁
소프트 단언 사용하기
테스트가 실패하면 Playwright가 어느 부분이 실패했는지 오류 메시지로 알려 줘요 — VS Code, 터미널, HTML 리포트, 트레이스 뷰어 중 어디서든 볼 수 있어요. 여기에 더해 소프트 단언을 쓰면, 실패해도 테스트 실행을 즉시 중단하지 않고 테스트가 끝난 뒤 실패한 단언 목록을 모아서 보여줘요.
// Make a few checks that will not stop the test when failed...
await expect.soft(page.getByTestId('status')).toHaveText('Success');
// ... and continue the test to check more things.
await page.getByRole('link', { name: 'next page' }).click();