통합 테스트(Integration Test)로도 잡기 힘듭니다. API 응답은 정상이니까요.
이럴 때 필요한 것이 E2E(End-to-End) 테스트입니다. 시스템의 처음(End)부터 끝(End)까지, 사용자가 사용하는 것과 똑같은 환경에서 시뮬레이션하는 것입니다. 실제 브라우저를 띄우고, 실제 클릭을 하고, 실제 DB를 다녀옵니다. 가장 확실한 검증 방법이죠.
2. 테스트 피라미드와 E2E의 위치
구글의 테스팅 권위자 마이크 콘(Mike Cohn)이 제시한 테스트 피라미드를 기억해야 합니다.
- 1층 (넓음) - Unit Test: 함수 단위 검증. 싸고, 빠르고, 많이 짬. (Jest, Vitest)
- 2층 (중간) - Integration Test: 모듈 간 연동 검증. (React Testing Library)
- 3층 (좁음) - E2E Test: 사용자 시나리오 검증. 비싸고, 느리고, 적게 짬. (Cypress, Playwright)
E2E는 강력하지만 비쌉니다. 실행 속도가 느리고, 유지보수가 어렵습니다. 그래서 "핵심 비즈니스 로직" 위주로 짜야 합니다. '로그인', '회원가입', '결제', '장바구니 담기' 같은 돈 버는 기능(Critical Path)은 반드시 E2E로 보호해야 합니다. 버튼 색상 바뀌는 걸 E2E로 검증하다간 테스트 돌리는 데만 3시간 걸리게 됩니다.
3. 도구의 전쟁: Cypress vs Playwright
예전에는 Selenium이 왕이었지만, 웹이 복잡해지면서 현대적인 도구들이 왕좌를 차지했습니다.
3.1. Cypress
- 장점: 프론트엔드 개발자 친화적입니다. 설정이 쉽고, 타임 트래블(Time Travel) 디버깅 기능이 기가 막힙니다. 테스트 코드가 브라우저 내부에서 돕니다.
- 단점: 브라우저 탭 제어 같은 일부 기능에 제약이 있고, 실행 속도가 상대적으로 느립니다.
3.2. Playwright (MS가 만듦)
- 장점: 사파리(WebKit), 파이어폭스 등 모든 브라우저 엔진을 완벽 지원합니다. 속도가 굉장히 빠르고 병렬 실행이 강력합니다. 요즘 대세로 떠오르고 있습니다.
- 단점: Cypress보다 디버깅 UI가 약간 덜 직관적일 수 있습니다. (하지만 빠르게 좋아지는 중)
3.3. Headless vs Headed 모드
- Headed Mode: 브라우저 창이 뜨고 클릭하는 게 눈에 보입니다. 개발할 때 씁니다.
- Headless Mode: 창을 띄우지 않고 백그라운드에서 실행합니다. CI/CD 서버에서 돌릴 때 씁니다. 훨씬 빠르고 리소스를 적게 먹습니다.
4. E2E 테스트 잘 짜는 법 (Best Practices)
4.1. 구현 상세에 의존하지 말 것
// 나쁜 예: CSS 클래스나 태그 구조에 의존
cy.get('.btn-primary').click();
cy.get('div > span:nth-child(2)').click(); // 구조 바뀌면 바로 깨짐
// 좋은 예: 사용자가 보는 텍스트나 의미에 의존 (접근성 고려)
cy.findByRole('button', { name: /로그인/i }).click();
cy.findByTestId('submit-button').click(); // 차라리 data-testid를 써라
테스트 코드는 내부 구현(HTML 구조)을 몰라야 합니다. 사용자는 div 구조를 모르고 "로그인 버튼"을 찾아서 누르니까요.
4.2. Flakiness(깨지기 쉬움)와의 전쟁
E2E 테스트의 최대 적은 '간헐적 실패(Flaky Test)'입니다.
네트워크가 0.1초 느려서, 애니메이션이 덜 끝나서 테스트가 실패합니다.
"아, 이거 원래 가끔 실패해요. 다시 돌리면 돼요." <- 이게 쌓이면 아무도 테스트 결과를 안 믿게 됩니다.
wait(1000)같은 고정 대기 시간을 쓰지 마세요.- "요소가 화면에 보일 때까지 기다림(Auto-waiting)" 기능을 제공하는 최신 툴을 믿으세요.
- 네트워크 요청을 가로채서(Intercept) 모의 응답(Mocking)을 주는 것도 방법입니다(외부 API 의존성 제거).
4.3. 테스트 데이터 격리
테스트 A가 DB에 데이터를 넣고 안 지우면, 테스트 B가 실패할 수 있습니다.
각 테스트는 독립적이어야 합니다.
beforeEach에서 DB를 초기화하거나, 테스트마다 고유한 유저(예: testuser_1234@gmail.com)를 생성해서 써야 합니다.
5. CI 파이프라인 최적화 (CI Funnel)
E2E 테스트는 느립니다. PR 올릴 때마다 30분씩 기다리면 개발자들이 싫어하겠죠. CI 퍼널(Funnel) 전략을 써야 합니다.
- Smoke Test: 핵심 기능(로그인, 결제)만 테스트. (항상 실행, 5분 이내)
- Regression Test: 전체 E2E 테스트. (밤에 한 번 실행, Nightly Build)
- Parallel Execution: GitHub Actions나 AWS CodeBuild에서 10대의 머신으로 쪼개서 돌립니다. Playwright는 샤딩(Sharding)을 기본 지원해서 50분 걸릴 걸 5분으로 줄여줍니다. 돈은 좀 들지만, 개발자 시간보다는 쌉니다.
6. 테스트 커버리지의 함정 (100%의 환상)
많은 팀이 "테스트 커버리지 100% 달성"을 목표로 합니다. 하지만 E2E 테스트에서 커버리지 100%는 불가능하고, 의미도 없습니다. 모든 버튼, 모든 에러 케이스를 브라우저로 클릭해서 테스트한다? 유지보수 비용 때문에 팀이 먼저 지쳐 떨어질 것입니다.
Pareto 법칙 (80:20 법칙)을 기억하세요. 20%의 핵심 기능이 매출의 80%를 만듭니다. 그 20%에 해당하는 Happy Path (성공 케이스)와 치명적인 Sad Path (실패 케이스)만 E2E로 막으세요. 나머지 자잘한 분기 처리는 Unit Test에게 맡겨야 합니다.
TDD와 E2E
보통 TDD(Test Driven Development)는 유닛 테스트 레벨에서 합니다. E2E로 TDD를 하기는 어렵습니다. 하지만 "개발 시작 전에 뼈대 E2E를 먼저 만드는 것"은 유용합니다. 기획서를 보고 "로그인하고 -> 상품 담고 -> 결제한다"는 빈 껍데기 테스트 코드를 짭니다. 처음엔 다 실패하겠죠(Red). 하나씩 기능을 붙여가며 통과시킵니다(Green). 이렇게 하면 기획서의 구멍을 코딩하기 전에 찾을 수 있습니다.
7. 마무리 - 자신감 있는 배포
금요일 오후 5시에 코드를 배포하고 퇴근할 수 있나요? E2E 테스트가 촘촘하게 박혀 있다면 가능합니다. "내가 수정한 코드가 기존의 회원가입 기능을 망가뜨리지 않았을까?"라는 불안감을 해소해주는 유일한 해독제니까요. 모든 것을 테스트하려 하지 말고, "이게 안 되면 우리 회사 망한다" 싶은 시나리오부터 작성해보세요.