실행 토폴로지와 러너 아키텍처
브라우저 러너, 서비스 클라이언트, 증거 수집기, 판정기를 어떤 구조로 분리해야 하는지 설명합니다.
핵심 요약
- 테스트 환경의 핵심은 러너 하나가 아니라 actuator·environment controller·evidence collector·judge·operator의 조합입니다.
- 디렉터리는 tool별이 아니라 runners·fixtures·contracts·evidence·runbooks 같은 책임 단위로 나눕니다.
- Playwright 설정도 사용법이 아니라 evidence pipeline과 gate semantics에 맞춘 actuator로 봅니다.
- matrix는 gate 브라우저 1개를 기본으로 device·locale·feature flag는 nightly/canary로 확장합니다.
- evidence는 trace(first retry)·screenshot(failure)처럼 실패 분류에 필요한 최소 세트로 과수집을 피합니다.
1.64.1 버전·브라우저·동시성 계약
@playwright/test와 해당 버전이 설치한 browser revision을 한 묶음으로 관리합니다. 이 책의 최신 기능 예제는 1.64.1을 확인 기준으로 삼고, 프로젝트에서는 검토한 버전을 --save-exact와 lockfile로 고정합니다. 다른 버전의 Playwright Docker image를 섞지 않습니다.
공식 CI 가이드는 worker 1개를 출발점으로 권장합니다. 먼저 runner 한 개의 실행을 안정화하고 shard job 수를 늘립니다. 4 shards×worker 6처럼 설정하면 DB·계정 경쟁이 24개로 커질 수 있습니다. 아래 예제의 global timeout 10분·job timeout은 팀 정책 예시이며 제품 제한이 아닙니다.
failOnFlakyTests는 재시도 후 성공한 테스트도 gate 실패로 반영합니다. flaky 결과를 허용할 suite는 별도 설정으로 나누고 승인된 격리 목록을 관리합니다. trace: 'on-first-retry'는 retry 0인 gate에서 trace를 남기지 않으므로 그런 gate에는 'retain-on-failure' 등 별도 정책을 정합니다.
CI guide · Playwright release notes.
테스트 환경의 핵심은 러너 하나가 아니라 액추에이터, 관측기, 판정기의 조합입니다. Playwright는 뛰어난 브라우저 액추에이터지만, 그것만으로 좋은 테스트 환경이 만들어지지는 않습니다.
테스트 환경 아키텍처의 기본 레이어
| 레이어 | 역할 | 예시 |
|---|---|---|
| actuator | 실제 상태를 바꾸거나 사용자 행동을 재현 | Playwright, API client, workflow trigger |
| environment controller | 데이터, clock, queue, sandbox를 준비 | seed tool, reset job, admin endpoint |
| evidence collector | trace, screenshot, logs, events, metrics 수집 | report pipeline, OTEL bridge, artifact store |
| judge / classifier | 실패 원인과 gate 의미 판정 | failure classifier, release policy engine |
| operator | 예외 승인과 인시던트 대응 | release manager, suite owner, on-call |
권장 디렉터리 구조
testops/
├── runners/
│ ├── browser/
│ ├── api/
│ └── workflow/
├── fixtures/
│ ├── auth/
│ ├── state/
│ └── sandbox/
├── contracts/
│ ├── ui/
│ ├── api/
│ └── events/
├── evidence/
│ ├── reporters/
│ ├── classifier/
│ └── retention-policy.md
└── runbooks/이 구조는 tool별 폴더 대신 테스트 환경이 어떤 책임으로 나뉘는지를 눈에 보이게 드러냅니다.
브라우저 러너 예시: Playwright
브라우저 layer를 Playwright로 구현할 때는 다음 정도면 충분합니다.
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined,
globalTimeout: process.env.CI ? 10 * 60 * 1000 : undefined,
failOnFlakyTests: !!process.env.CI,
reporter: process.env.CI
? [['list'], ['blob']]
: [['list'], ['html', { open: 'never' }]],
use: {
baseURL: process.env.E2E_BASE_URL ?? 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium-smoke',
grep: /@smoke/,
use: { ...devices['Desktop Chrome'] },
},
{
name: 'chromium-critical',
grep: /@critical/,
use: { ...devices['Desktop Chrome'] },
},
],
})여기서 중요한 건 이 설정이 "Playwright 사용법"이 아니라, browser actuator가 evidence pipeline과 gate semantics에 맞게 움직이도록 맞춰 둔 것이라는 점입니다.
matrix 원칙
| 축 | 기본 원칙 | 이유 |
|---|---|---|
| 브라우저 | gate는 1개, nightly에서 확장 | gate 신뢰도 우선 |
| device | critical 일부만 모바일 view 확대 | 비용과 triage 시간 절감 |
| locale | 법률/결제/권한처럼 민감한 흐름만 확대 | 조합 폭발 방지 |
| feature flag | canary / rehearsal 중심 | 상시 matrix 최소화 |
evidence 정책
| evidence | 기본 정책 | 목적 |
|---|---|---|
| trace | first retry 이상 | root cause 파악 |
| screenshot | failure only | 시각 상태 확인 |
| video | retain on failure | 동적 문제 파악 |
| console / network log | gate suite 중심 | JS/API 오류 분류 |
| event / job log | async flow 중심 | workflow completion 확인 |
evidence 과수집 주의
실행마다 모든 artifact를 남기면 저장 비용이 늘 뿐 아니라 triage 속도까지 느려집니다.
evidence는 실패를 분류하는 데 필요한 최소 세트를 기준으로 설계합니다.
아키텍처 리뷰 체크리스트
- actuator와 judge가 섞여 있지 않은가?
- evidence만 보고도 실패를
product / data / infra / external로 분류할 수 있는가? - 공용 fixture가 제품 로직을 너무 많이 숨기고 있지 않은가?
- 브라우저, API, workflow probe가 같은 gate vocabulary를 쓰고 있는가?
공유 자원은 test lock과 데이터 격리를 구분
Playwright 1.63+는 같은 이름의 test lock을 선언한 테스트를 파일·worker·project 사이에서 직렬화합니다.
import { test, expect } from '@playwright/test'
test('공유 설정 확인', { lock: 'billing-settings' }, async ({ page }) => {
await page.goto('/settings/billing')
await expect(page.getByRole('heading', { name: 'Billing' })).toBeVisible()
})별도의 CI job·shard·외부 프로그램까지 보호하는 DB 분산 락으로 간주하지 않습니다. shard별 계정·tenant·데이터 lease가 먼저이며 test lock은 동일 실행 안의 남은 경쟁을 줄이는 수단입니다.