"Flake 는 Playwright 문제 아냐. 너의 테스트가 가시화하는 설계 문제야."
Flake 의 흔한 다섯 원인
- 하나 가정하는데 여러 element 매칭하는 selector. 'Save' 이름의 버튼 두 개, 같은 텍스트의 리스트 아이템 두 개 — locator 가 레이아웃 바뀌어서 다른 첫 번째 매칭할 때까지 첫 번째 조용히 매칭.
- 테스트 순서 의존성. 테스트 A 가 사용자 생성; 테스트 B 가 사용자 카운트 1 단언. B 만 돌리면 실패; A 그 다음 B 면 통과; 병렬은 동전 던지기.
- State 단언 대신 sleep. 지난 lesson 에서 다룸. 추측 지속 시간은 일어날 race.
- 시간 / 날짜 의존. '오늘의 아이템' 필터 테스트가 UTC 자정에 깨져. 너의 코드와 같음 — 테스트가 실제 시간 쓰면 자정 버그 보게 돼.
- 진짜 네트워크 호출. 진짜 API 치는 테스트는 그 API 가 살아 있고, 빠르고, 결정적인 데 의존. 보통 다 참 아냐.
해독제 — 테스트 격리
모든 Playwright 테스트가 default 로 자기 브라우저 컨텍스트 — 별개 쿠키, 별개 localStorage, 별개 캐시. 그게 격리의 기초. 근데 격리는 테스트가 깊게 가는 만큼만 깊어: 테스트가 DB row 생성하면 뭔가 정리하지 않는 한 그 row 남아.
깨끗한 패턴: 모든 테스트가 자기 state setup 하고 tear down (또는 테스트 사이 reset 되는 DB 스냅샷에 대해 작업). 다른 테스트도 쓰는 'fixture' 에 대해 테스트하지 마.
해독제 — 결정적 시간과 데이터
테스트가 의존하면 시간과 데이터 핀. Playwright 에 페이지 안 wall-clock 시간 고정 위한 page.clock.install() 있어 (Vitest 의 useFakeTimers 와 비슷). MSW (다음 트랙) 가 네트워크 쪽 커버 — 모든 API response 가 네가 제어.
해독제 — 구체적 locator
Locator 가 한 element 이상 매칭하면 테스트가 암묵적으로 '첫 번째' 선택 — 오늘 DOM 이 우연히 첫 렌더하는 거. 정확히 한 매칭 있게 selector 충분히 구체적으로:
- 부모 scope 추가: 그냥
page.getByRole('link', { name: 'Home' })아니라page.getByRole('navigation').getByRole('link', { name: 'Home' }). - 리스트 안 컨텐츠 기반 disambiguation 엔
filter({ hasText })써. - 텍스트 매칭 시
{ exact: true }옵션 써:getByText('Save', { exact: true })는 'Save and continue' 매칭 안 함.
모든 flake 를 설계 신호로 다뤄, 일시적 짜증 아니라. 본능은 retry 를 2 에서 5 로 올리고 넘어가는 거. 규율은 묻기: 뭐가 race 했어? 어떤 state 가 공유됐어? 어떤 selector 가 모호했어? 제대로 고친 flake 하나가 버그 클래스 제거; retry 로 반창고 붙인 거 하나가 더 많은 flake 양육.
해독제 — 조사 도구
테스트가 flake 하면 trace viewer 가 첫 정거장. 보여줘:
- Locator 가 쿼리된 순간의 DOM — element 가 거기 있었어?
- 진행 중 네트워크 요청 — API 가 다르게 응답했어?
- 액션 타임라인 — 클릭이 맞는 element 에 떨어졌어?
trace: 'on-first-retry' 활성화해서 retry 에서 trace 자동 캡처, 거기서부터 디버그.
Flake 분류
Flake 보면 순서대로 물어:
- Locator 가 여러 element 매칭해? (Trace 열고 DOM 봐.)
- 테스트가 다른 테스트의 state 에 의존해? (격리 실행; 통과하면 찾은 거.)
- 인터랙션 전에
waitForTimeout이나 누락된 단언 있어? (Lesson 7.3 참고.) - 테스트가 시간이나 진짜 네트워크에 의존해? (시간 핀, 네트워크 mock.)