
AI가 리뷰한 Playwright/Cypress PR 100건 중 33건에서 발견된, 실패할 수 없는 E2E 테스트
요약
Playwright와 Cypress를 사용한 E2E 테스트에서 발생하는 '사일런트 패스(silent pass)' 오류 사례를 분석합니다. .only 사용, 잘못된 truthy 검증, 비동기 await 누락 등으로 인해 테스트가 실제로는 실패했음에도 불구하고 성공으로 표시되는 위험성을 경고합니다.
핵심 포인트
- it.only 사용 시 나머지 테스트가 무시되어 CI가 허위로 통과될 수 있음
- page.locator()는 요소가 없어도 truthy를 반환하므로 toBeVisible() 사용 권장
- Promise를 반환하는 메서드 사용 시 반드시 await를 통해 결과를 확인해야 함
- 결과값을 반환만 하고 assert하지 않는 코드는 테스트의 의미를 상실함
브라우저에서 실행되는 VS Code인 code-server(78k 스타)의 테스트를 디버깅하던 누군가가 우리 모두가 흔히 저지르는 실수를 했습니다. 반복 속도를 높이기 위해 테스트를 단 하나에만 집중(focus)시킨 것입니다.
it.only("should change to expired when not active", async () => {
그리고 그것이 커밋되었습니다. 약 7개월 동안, 그 하나의 .only가 동일한 파일의 나머지 8개 테스트를 조용히 무효화하고 있었습니다. 그동안 CI(지속적 통합)는 계속 초록색(pass) 상태였습니다. CI 입장에서는 아무런 문제가 없었기 때문입니다. 포커스된 테스트가 실행되었고, 통과했다, 그게 전부였습니다. 테스트 스위트(suite)가 다시 활성화되었을 때, 스킵되었던 테스트 중 하나는 이미 망가져서 더 이상 통과하지 못하는 상태였습니다. 반년 넘게 모든 사람이 받았던 유일한 신호는 초록색 체크 표시뿐이었습니다.
수정 사항은 이미 머지(merge)되었습니다(coder/code-server#7845. 이 PR에서는 E2E 스위트 내의 일회성 isVisible() 읽기나, matcher가 없는 expect() 호출도 함께 수정했습니다). 오해를 피하기 위해 말씀드리자면, 이것은 code-server가 부주의하다는 뜻이 아닙니다. 제가 리뷰해 온 프로젝트 중에서도 매우 잘 관리되는 프로젝트 중 하나입니다. 그렇기 때문에 첫 번째 사례로서 가치가 있습니다. 그 정도 규모의 리포지토리(repository)에서도 잘못 들어간 .only가 7개월 동안이나 발견되지 않고 남아있을 수 있다면, 그것은 어디에서든 일어날 수 있는 일입니다.
동일한 버그가 다른 4개의 프로젝트에서도 발견됨
아무런 증명 없이 통과해 버리는 테스트를 찾기 시작하자, 잘 관리되고 널리 사용되는 코드베이스에서 차례차례 발견되었습니다. 이 유형 전체를 한 줄의 머지된 수정 사항으로 응축한 것이 Carbon Design System의 이 사례입니다(carbon#22564).
- expect(page.locator('.cds--progress-step--complete')).toBeTruthy();
+ await expect(page.locator('.cds--progress-step--complete')).toBeVisible();
page.locator()는 페이지에 어떤 상황이 발생하더라도 Locator 핸들을 반환합니다. 요소가 존재하지 않더라도, 기능 전체가 삭제되었더라도, 핸들은 truthy 상태를 유지합니다. 원래의 어서션(assertion)은 단 한 번도 실패한 적이 없으며, 앞으로도 실패하지 않을 것입니다.
Ghost에는 그 비동기(asynchronous) 버전이 있었습니다(Ghost#28712).
expect(likeButton.isDisabled()).toBeTruthy();
isDisabled()는 Promise를 반환하며, Promise는 항상 truthy입니다. 버튼이 disabled 상태이든, enabled 상태이든, 혹은 아예 렌더링되지 않았더라도 이 어서션은 통과합니다.
Strapi에서는 계산된 결과를 그대로 버리고 있는 체크가 있었습니다(strapi#26630). isVisible(), isHidden(), isEnabled()를 호출해 놓고는 그 결과를 허공에 날려버릴 뿐, 어서트는 전혀 하지 않고 있었습니다. SvelteKit에서는 web-first 어서션에 await가 붙어 있지 않았습니다(kit#16068). 그 때문에 어서션은 공중에 붕 뜬 상태로 남아 테스트 결과에 전혀 관여하지 않았습니다.
겉모습은 다른 버그들이지만, 실패 모드는 하나입니다. 테스트가 실행되고, 초록색이 되지만, 그 초록색에는 아무런 의미가 없습니다. 저는 이것을 '사일런트 패스 테스트(silent pass test)'라고 부르기 시작했습니다. 까다로운 점은 그 위장성입니다. 테스트가 누락되어 있다면 적어도 '구멍'으로 보일 것입니다. 하지만 사일런트 패스 테스트는 커버리지(coverage)가 있는 것처럼 보이게 만듭니다. 게다가 드문 일도 아닙니다. 뒤에서 언급할 100개의 PR 벤치마크 코퍼스(corpus)에서는, Playwright 또는 Cypress의 spec을 다루는 100건의 PR 중 33건이 이러한 종류의 실제 문제를 적어도 하나씩 포함하고 있었습니다.
5분 만에 자신의 스위트를 점검하기
기계적인 케이스라면 제 도구는 필요 없습니다. 필요한 것은 grep입니다. 다음 4가지는 트리아지(Triage)용입니다. 정당한 행도 몇 개 걸릴 수 있습니다(expect(configObject).toBeDefined()는 문제없지만, Locator에 대한 그것은 장식에 불과합니다). 건당 몇 초면 육안으로 확인할 수 있습니다. 다음은 Playwright용입니다. Cypress 버전은 리포지토리에 있습니다.
# 1. Committed focused tests: CI runs one test, silently skips the siblings
grep -rnE "(it|test|describe)\.only\(" e2e/
# 2. Truthiness asserted on a Locator handle: always passes
...
#1 또는 #2로 확인된 히트(hit)는, 현재 아무것도 증명하지 못하고 있는 테스트, 혹은 스위트(suite) 전체입니다.
첫 번째 시도: 어쨌든 lint 하기
처음에 제가 생각한 것은 이것이 lint의 문제라는 것이었습니다. 실제로 일부는 정말 그렇습니다. expect(locator).toBeTruthy()는 구문적인 형태입니다. 끼어든 .only는 이보다 더 기계적일 수 없습니다. 그래서 저는 그 레이어를 만들었고, 두 개의 ESLint 플러그인인 eslint-plugin-playwright-silent-pass와 eslint-plugin-cypress-silent-pass를 공개했습니다. 이것들은 Carbon이나 Ghost 같은 버그를 이후의 모든 PR에서 플래그(flag)합니다. (설치를 위한 원라이너(one-liner)는 끝에 있습니다. 위의 grep은 설치가 필요 없는 버전입니다.)
그리고 저는 벽에 부딪혔습니다. 삭제 버튼을 클릭하고, 모달이 닫히기를 기다린 뒤 종료되는 테스트를 생각해 보세요. 어떤 행도 관용구(idiom)대로 작성되었습니다. 존재하는 어설션(assertion)은 모두 await 되어 있으며 올바릅니다. 문제는 존재하지 않는 어설션 쪽에 있습니다. 대상 엔티티가 사라졌다는 것을 아무것도 검증하지 않고 있는 것입니다. 이것은 어떤 AST 규칙으로도 플래그할 수 없습니다. 지시해야 할 '나쁜 노드'가 존재하지 않으며, 결함은 노드의 부재 그 자체이기 때문입니다. '이용 약관에 동의합니다'라는 이름임에도, 동의 전의 상태만을 어서트(assert)하고 있는 테스트도 마찬가지입니다. 고장 난 인증 엔드포인트를 그대로 통과시켜 버리는 expect([200, 401, 403]).toContain(status)와 같은 API 체크도 유사합니다. 이러한 케이스를 판단하려면 리뷰어가 하는 것처럼 테스트를 읽어야 합니다. 이 테스트가 무엇을 증명한다고 주장하고 있으며, 코드가 그것을 정말로 증명하고 있는가 하는 점 말입니다.
이것은 의미론적인 판단이며, 엄격한 제약을 가하기만 한다면 LLM이 잘하는 종류의 일입니다.
주장이 아니라, 측정하기
"LLM이 당신의 테스트를 리뷰한다"라는 말은, 대충 말하자면 얼마든지 할 수 있는 주장입니다. 그래서 저는 이 글을 쓰기 전에 이 접근 방식을 벤치마크(benchmark)하였고, 숫자를 재현할 수 있도록 케이스별 근거를 공개했습니다 (방법론과 결과).
셋업은 다음과 같습니다. 77개의 서로 다른 리포지토리에 걸친 100건의 오픈 소스 PR. 모두 Playwright 또는 Cypress의 spec 파일을 다루고 있으며, 모두 8종류의 AI PR 리뷰 봇(CodeRabbit, Copilot, Codex, Sourcery, Gemini, Cursor, Ellipsis, Greptile) 중 하나로 이미 리뷰를 마친 상태입니다. LLM 저지(judge)가 모든 spec 파일을 읽고, 110건의 실제 "테스트 신뢰성"과 관련된 문제로 구성된 그라운드 트루스(ground truth)를 확립하였으며, 이를 기준으로 3개의 도구를 채점했습니다.
| 도구 | 탐지한 실제 문제 | 위양성 (False Positive) | 단독 탐지 |
|---|---|---|---|
| e2e-reviewer | 78 / 110 (71%) | 0 | 47 |
| ... |
위양성 (False Positive) 열의 0에 대하여. 이는 제 리뷰어가 보고한 지적 중, 저지(judge)가 "부적절함"이라고 판정한 것이 단 하나도 없었다는 의미입니다. 그럼에도 이 셀은 의심해 보아야 하며, 그 이유는 아래의 두 번째 단서 조항에 있습니다. PR 단위로 보면, 실제 문제를 포함하고 있던 33건의 PR에서의 승패는 e2e-reviewer가 19승, lint만으로 충분했던 경우가 11건, AI 리뷰어가 2건, 무승부가 1건이었습니다.
다음은 단서 조항입니다. 숫자는 그 약점에 해당하는 부분만 신뢰할 수 있기 때문입니다.
AI 리뷰어의 숫자는 과소평가되었습니다. 봇은 PR 전체에 주의를 분산시키며 리뷰하고 있으며, 이번에 카운트한 것은 spec 파일에 대한 인라인 코멘트(inline comment)뿐이었고, 심지어 PR당 최대 6개로 제한했습니다. 그들의 "노이즈" 중 상당수는 정당한 피드백임에도 불구하고, 이번 그라운드 트루스(ground truth) 관점에서는 단순히 부적절했던 것들입니다. 이는 전문 특화 도구를 그들의 전문 분야(홈그라운드)에서 범용 도구와 비교했을 뿐입니다. 그 이상의 의미는 없습니다. -
저지와 제 리뷰어는 동일한 모델 패밀리입니다. 이는 재현율(recall)을 부풀리고 위양성(false positive)을 실제보다 낮게 보이게 할 가능성이 있습니다. 압박 테스트(pressure test)로서, 논란의 여지가 있는 15건의 단독 탐지 건을 다른 모델의 저지(Codex를 통한 OpenAI의 gpt-5.5)에게 재판정하게 한 결과, 15건 중 13건이 지지되었습니다. 의견이 엇갈린 2건은 정의상의 에지 케이스(edge case)였으며, 결함 자체가 뒤집힌 것은 아닙니다. -
그라운드 트루스는 100개의 PR이라는 하나의 스냅샷에 대해 LLM이 확립한 것입니다. 개별적인 정확한 숫자는 확정판이 아닌 지표로 취급해 주십시오.
제 제품에 불리한 결과가 두 가지 있으며, 이 또한 기사에 기재해 두고 싶습니다. 첫 번째, 순수하게 기계적인 레이어에서는 제 스캐너가 표준적인 lint에 대해 새로운 지적을 추가할 수 있었던 경우가 100건 중 단 8건의 PR뿐이었습니다. 우선 lint를 먼저 돌리십시오. 진심으로 드리는 말씀입니다. 47건의 단독 탐지는 모두 의미론적(semantic) 레이어에서 나왔습니다. 벽은 실재하며, 가치가 있는 곳은 바로 그 지점입니다. 두 번째, 벤치마크는 이 도구를 축소시켰습니다. 봇의 코멘트에서 추출한 3가지 후보 규칙(커스텀 sleep 헬퍼, 외부 URL로의 네비게이션, 하드코딩된 localhost URL)은 77개의 리포지토리에서 측정한 결과 모두 기각되었습니다. 출현율이 거의 0이거나, 프로젝트 설정을 고려했을 때 위양성(false positive)이 100%에 가깝기 때문입니다. 이 데이터 덕분에 도구는 더 정교해졌습니다.
이 지적은 저와 협력할 의무가 없는 메인테이너들에게도 통했습니다. 본문 작성 시점을 기준으로, 사이런트 패스(silent pass) 테스트를 수정한 업스트림(upstream) PR이 12건 리뷰를 거쳐 머지(merge)되었습니다. Storybook(90k stars), code-server(78k), Strapi(72k), Ghost(54k), Cal.com(45k), Bruno(45k), Qwik(22k), SvelteKit(20k), Element Web(13k), Carbon Design System(9k), MUI X(5.8k), 그리고 module-federation/core(2.6k). 추가로 여러 건이 리뷰 중입니다 (케이스 스터디). 이들 모두 CI에서는 통과하고 있었지만, 증명하는 내용이 너무나 부족했던 테스트를 수정한 것입니다.
또 하나, "AI 지원 PR"은 2026년에 상당한 평판(악명)을 얻게 될 것이므로 미리 말씀드립니다. 이것들은 무차별적으로 뿌려진 것이 아닙니다. 모든 수정 사항은 대상 리포지토리의 컨트리뷰션 가이드라인(contribution guidelines)에 맞춰 준비되었으며, 제출 전에 검증되었습니다. 집계 결과는 모두 공개하고 있습니다. 머지 완료 12건, 리뷰 중 8건입니다. 무엇이 거절되었고 그 이유는 무엇인지까지 포함한 파이프라인 전체는 로드맵에 있습니다.
만든 것

e2e-skills는 Claude Code, Codex 및 기타 스킬 대응 에이전트를 위한 4가지 Agent Skills로 구성되어 있으며, 24개의 E2E 안티 패턴 (Anti-pattern)을 P0/P1/P2로 등급을 매긴 분류 체계를 핵심으로 구축되었습니다. P0는 사이런트 상시 통과 (Silent always pass), 즉 실패해서는 안 되는 테스트입니다. P1은 실패했을 때 진단 가능성이 낮은 테스트이며, P2는 유지보수 부채 (Maintenance debt)입니다.
핵심인 e2e-reviewer는 두 단계로 작동합니다. 먼저 결정론적인 스캐너 (순수 bash, 정규 표현식 및 ast-grep 사용, LLM 없이 단독 실행 가능)가 후보를 생성합니다. 이어지는 의미론적 리뷰 (Semantic review) 단계에서 각 후보를 문맥 속에서 검증하며, 스캐너가 포착할 수 없는 '부재로서의 결함'—예를 들어, 삭제를 한 번도 확인하지 않는 삭제 테스트—을 찾아냅니다. 이 검증 단계 덕분에 오탐 (False positive)의 수가 0이 됩니다. LLM의 역할에는 새로운 문제를 찾는 것뿐만 아니라, 타당하지 않은 후보를 제거하는 것도 포함되기 때문입니다. 나머지 3가지 스킬은 생성과 실패 조사를 담당합니다.
playwright-test-generator는 동일한 24가지 패턴 기준을 충족하는 새로운 커버리지 (Coverage)를 작성합니다. 최초 실행 시에는 프로젝트의 규칙 (AGENTS.md의 E2E 섹션 및 시드 spec)을 스캐폴딩 (Scaffolding)하며, 마지막으로 YAGNI 감사를 통해 아무도 호출하지 않는 Page Object를 배포하는 일이 없도록 마무리합니다. playwright-debugger와 cypress-debugger는 이미 보유하고 있는 리포트 파일 (playwright-report/, mochawesome 또는 JUnit)에서 시작하여, 각 실패를 15가지 근본 원인 카테고리—플래키한 타이밍 (Flaky timing), 셀렉터 드리프트 (Selector drift), 환경 불일치 등—중 하나로 분류하고 각각에 구체적인 수정 방안을 덧붙입니다. GitHub Actions의 run을 전달하면 아티팩트 (Artifact)를 스스로 가져옵니다. 대상은 Playwright와 Cypress로만 한정되어 있는데, 이는 의도적인 선택입니다. 그 외의 도구에 대해서는 스킬이 추측하지 않고 "범위 외 (out of scope)"라고 답변합니다.
우선 위의 grep을 자신의 테스트 스위트 (Suite)에 돌려보세요. 향후 모든 PR에서 기계적인 패턴을 잡아내고 싶다면, 가장 가벼운 진입로는 린트 (Lint) 레이어입니다.
npm i -D eslint-plugin-playwright-silent-pass # 또는 eslint-plugin-cypress-silent-pass
스킬, 스탠드얼론 스캐너 (Standalone scanner), 그리고 벤치마크의 모든 근거는 github.com/voidmatcha/e2e-skills 에 있습니다. 두 가지 린트 플러그인도 오픈 소스입니다: playwright · cypress.
이 습관은 아무것도 설치하지 않고도 기를 수 있습니다. 다음에 테스트 스위트가 그린 (Green) 상태가 되었을 때, 스스로에게 이렇게 물어보세요. "만약 오늘 밤 이 기능이 고장 난다면, 실제로 실패할 테스트는 무엇일까?"라고.
리포지토리: https://github.com/voidmatcha/e2e-skills (영어 버전 기사는 dev.to에 있습니다)
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기