다음은 공유 데이터로 흔들리는 E2E 테스트를 설명하기 위한 예시입니다. 주문 저장 테스트가 간헐 실패해 버튼 클릭 전 고정 대기를 넣었지만 문제는 사라지지 않습니다. 원인은 버튼이 늦게 나타나는 것이 아니라 이전 테스트의 주문 번호가 남아 API가 409를 반환하는 데 있습니다.
핵심 안정적인 E2E 테스트는 오래 기다리는 테스트가 아니라 매번 새 상태에서 시작하고 사용자가 보는 이름으로 조작하며 실패 증거를 남기는 테스트입니다. 재시도는 결함을 숨기는 통과 장치가 아니라 불안정성을 분류하는 마지막 관찰 수단으로 씁니다.
흔들리는 테스트는 Trace를 읽는 세션부터 엽니다
URL과 테스트 이름은 예시입니다. 저장소의 실제 명령을 사용합니다.
claude --permission-mode plan
저장 실패 E2E의 간헐 실패를 조사하세요. 아직 수정하지 마세요.
읽기: CLAUDE.md, playwright.config.*, 대상 spec, fixture, 첫 실패 Trace
허용: Read, Grep, 대상 테스트 1회와 Trace 조회
금지: waitForTimeout 추가, 재시도 상향, 운영 URL·실계정 사용
반환: 실패 시점 / 공유 상태 / locator·응답 대기 문제 / 최소 후보
중단: 격리된 테스트 데이터나 승인된 테스트 URL이 없을 때
첫 실패 Trace가 고칠 축을 하나로 좁힙니다
Trace는 실패 당시의 요청, 화면 구조와 콘솔을 묶어 보는 기록이고, Locator는 버튼의 역할·이름처럼 사용자가 인식하는 단서로 요소를 찾는 방식입니다. 첫 Trace가 중복 주문 409를 보여 준다면 데이터 격리만 고칩니다. 선택자와 대기 방식까지 함께 바꾸면 무엇이 간헐 실패를 없앴는지 판정할 수 없습니다.
재시도에서만 통과하거나 Trace가 남지 않거나 외부 서비스가 실제 효과를 만들면 중단합니다. 재개 메모에는 첫 실패 Trace 경로와 아직 대체하지 못한 의존성을 적습니다.
고정 대기는 중복 응답을 해결하지 못합니다
예시의 기존 테스트는 `button:nth-child(2)`를 누르고 일정 시간이 지난 뒤 성공 문구를 찾습니다. 버튼 순서가 바뀌면 다른 버튼을 누를 수 있고, 앞 테스트가 만든 주문 번호가 남으면 서버는 중복으로 거절합니다. 화면 속도 문제가 아닌데 시간을 늘려 우연히 통과할 기회만 늘린 셈입니다.
Playwright는 Locator의 동작 가능 상태와 웹 우선 단언을 자동으로 다시 확인합니다. 여기에 임의 대기를 겹치기보다 테스트 데이터와 선택자를 고칩니다. BrowserContext는 테스트마다 별도 브라우저 사용자 공간을 만들어 쿠키와 로컬 저장소를 나누지만, 서버 DB의 주문 행까지 지워주지는 않습니다.
주문 번호를 실행마다 다르게 만들고 결과를 기다립니다
버튼은 사용자가 인식하는 역할과 이름으로 찾습니다. 저장 요청의 응답을 클릭과 동시에 기다리면 “화면이 언젠가 바뀌겠지”가 아니라 해당 요청이 성공했는지 확인할 수 있습니다.
import {test, expect} from '@playwright/test';
test('주문을 저장하면 상세 화면이 열린다', async ({page}, testInfo) => {
const orderNo = `E2E-${testInfo.workerIndex}-${Date.now()}`;
await page.goto('/orders/new');
await page.getByLabel('주문 번호').fill(orderNo);
const response = page.waitForResponse(r =>
r.url().endsWith('/api/orders') && r.request().method() === 'POST'
);
await page.getByRole('button', {name: '주문 저장'}).click();
expect((await response).status()).toBe(201);
await expect(page.getByRole('heading', {name: orderNo})).toBeVisible();
});
실패 데이터가 테스트 밖으로 새지 않게 닫습니다
고유 번호만으로 충분하지 않은 서비스라면 API 픽스처로 테스트 전 주문을 만들고 테스트 후 해당 ID만 제거합니다. 전체 테이블 초기화는 병렬 테스트를 서로 지웁니다. 정리 실패도 로그에 남기되 원래 테스트 실패를 덮지 않게 합니다.
로그인 상태는 공식 `storageState`를 재사용할 수 있지만, 권한을 검증하는 테스트끼리 같은 상태 파일을 공유하면 역할이 섞입니다. 관리자, 일반 사용자, 미로그인 상태를 별도 파일로 만들고 비밀값이 저장소에 올라가지 않게 제외합니다.
재시도 통과를 성공으로 세지 않습니다
Playwright는 기본적으로 실패 테스트를 재시도하지 않습니다. 재시도를 켰을 때 첫 실행 실패 후 통과한 테스트는 `flaky`로 분류됩니다. 이 예시의 기대 결과는 수정 전 반복 실행에서 간헐 통과하던 테스트가 데이터 격리 뒤 첫 실행부터 안정적으로 통과하는 것입니다.
재시도 횟수를 늘려 초록색 비율만 높이면 실제 사용자의 간헐 오류를 숨길 수 있습니다. 새 간헐 통과가 나오면 병합을 중단하고 Trace의 요청, DOM 스냅샷과 콘솔을 확인합니다.
Trace는 모든 성공 실행보다 첫 재시도에 남깁니다
CI에서는 첫 재시도에 Trace를 기록하면 평소 저장 비용을 줄이면서 최초 실패와 재시도 상태를 비교할 수 있습니다. 실패가 재현되지 않더라도 Trace가 어떤 요청과 요소 상태가 달랐는지 보여 줍니다.
import {defineConfig} from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 1 : 0,
use: {trace: 'on-first-retry'},
reporter: [['html', {open: 'never'}]],
});
E2E로 옮기지 말아야 할 검사도 있습니다
가격 계산의 여러 경계값, 문자열 변환과 순수 함수는 단위 테스트가 더 빠르고 원인이 선명합니다. E2E는 로그인, 주문 저장, 결제 결과처럼 브라우저·서버·DB가 연결될 때만 생기는 핵심 흐름에 씁니다. 외부 결제사의 실제 결제를 CI마다 호출하는 것도 중단 대상입니다. 계약된 테스트 환경이나 응답 대체를 사용합니다.
2026년 7월 29일 Playwright 공식 문서의 테스트 격리, Locator, 재시도와 Trace 정책을 확인했습니다. 완료 결과에는 첫 실행 통과율, 반복 횟수, 남긴 Trace와 실제로 대체한 외부 의존성을 함께 적어야 합니다.