1편에서 단일 제품 자동화를 처음 구축했고, 2편에서 여러 제품으로 확장하는 프레임워크 구조를 다뤘다. 구조 얘기는 충분히 했으니, 이번엔 TC 하나를 실제로 어떻게 코드로 만들고 CI까지 올리는지를 다룬다.
스크립트 작성 방식 두 가지
우리 팀은 두 가지 방식을 혼용한다.
.spec.ts 방식은 가장 빠르다. BDD 흐름은 주석으로 표현한다.
test('동적 탐지 결과 검색', async ({ page }) => {
/* Given 탐지 기간 설정 */
await page.goto('incidents/alerts');
/* When 필터 선택 */
await page.getByText('특정 탐지 항목', { exact: true }).click();
/* Then 결과 테이블 검증 */
await expect(page.getByRole('table')).toContainText('특정 탐지 항목');
});
.feature 방식은 팀원이 비개발자일 때 유용하다. Gherkin 구문으로 시나리오를 작성하면 playwright-bdd가 실행 가능한 코드를 자동 생성한다. 개발자 중심 팀이라면 솔직히 .spec.ts가 훨씬 빠르다.
Codegen 초안을 그대로 쓰면 안 되는 이유
Playwright Codegen으로 클릭하면 초안이 뚝딱 나온다. 근데 그걸 그대로 커밋하면 CI에서 자주 깨진다.
Codegen은 지금 이 순간의 DOM 구조로 셀렉터를 잡는다. UI가 조금만 바뀌면 바로 터진다. 실제로 이런 코드가 생성된 걸 본 적 있다.
// ❌ UI 변경 한 번이면 끝
page.locator('div:nth-child(6) > ul > li:nth-child(2)')
page.getByRole('treeitem', { name: 'caret-down 메뉴명' })
초안에서 이런 셀렉터를 찾아서 안정적인 버전으로 바꿔야 한다.
// ✅ 이걸 써야 UI가 바뀌어도 살아남는다
page.getByRole('button', { name: '저장' })
page.getByText('정책 이름', { exact: true })
page.getByTitle('삭제')
페이지 이동, 검색 버튼 클릭, 저장 후에는 네트워크 대기도 반드시 넣는다. 로컬에서는 넘어가도 CI 서버에서 타임아웃이 나는 경우가 의외로 많다.
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
로컬에서 확인하는 방법
# 화면 보면서 실행
npx playwright test tests/4_Detection/search.spec.ts --headed
# BDD Feature 실행
npx bddgen && npx playwright test .features-gen/
# 실패 케이스 Trace 확인
npx playwright show-trace test-results/<실패_폴더>/trace.zip
Trace 뷰어가 생각보다 유용하다. 각 액션 시점의 DOM 스냅샷, 네트워크 요청, 콘솔 로그를 타임라인으로 볼 수 있어서 원격 CI에서 실패한 케이스도 로컬에서 재현할 수 있다.
CI 환경에서 주의할 설정
timeout: 70000, // 로컬보다 넉넉하게
retries: process.env.CI ? 2 : 0, // CI에서만 재시도 2회
forbidOnly: !!process.env.CI, // test.only 실수 방지
workers: globalThis.workerCount, // CPU 코어 기반 자동 설정
screenshot: 'only-on-failure',
video: { mode: 'retain-on-failure' },
trace: 'retain-on-failure',
retries: 2는 간헐적인 네트워크 지연을 걸러낸다. 근데 retry로 통과하는 케이스가 많아진다면 그건 TC 자체의 문제다. 신호로 읽어야 한다.
forbidOnly는 test.only를 실수로 커밋했을 때 CI 빌드를 즉시 실패시킨다. 한 번 겪어보면 왜 이게 필요한지 바로 납득된다.
반복해서 만나는 실패 패턴
| 증상 | 원인 | 해결 |
|---|---|---|
locator.click: Test ended / Timeout |
접힌 트리 노드 또는 Codegen 생성 셀렉터 | getByText('이름', { exact: true }) 또는 getByTitle()로 교체 |
ReferenceError: X is not defined |
코드 끝 오타 (한글 자음, 백틱 등) | 해당 라인 오타 제거 |
toContainText 원격에서만 실패 |
로컬↔CI 서버 간 DB 데이터 표기 차이 | 하드코딩 문구 완화 |
toBeVisible 원격 타임아웃 |
CI 서버 네트워크·렌더링 지연 | 검증 전 waitForLoadState('networkidle') 추가 |
| Headless에서만 SVG/말줄임표 깨짐 | Headless 렌더링 차이 | .first() 계열로 변경 |
구조보다 중요한 건 유지보수성이다. 완벽한 TC보다 내일도 고칠 수 있는 TC가 낫다.
다음 편에서는 브라우저 바깥의 영역을 다룬다. 탐지 결과를 확인하려면 먼저 탐지가 일어나야 하는데, 그 탐지를 원격 에이전트에서 직접 일으켜야 하는 상황이 있다.