
'E2E 100% 통과'의 정체: expect()가 0개인 러너 7개를 지운 날
2025년 8월부터 'E2E 100% 통과'의 근거였던 러너 7개에는 단언이 하나도 없었고, 지운 그날 밤 실제 소켓으로 돈 E2E가 결함 7건을 적었습니다.
2026년 9월 6일 저녁, P2P 채팅 앱(Flutter) 저장소에서 E2E 러너 7개, 3,010줄을 지웠습니다. 이 파일들은 2025년 8월 커밋의 "370회 테스트 100% 성공률"과 "E2E 테스트 100% 통과"의 근거였습니다. 7개 모두 expect()가 0개였습니다. 같은 날 저녁 flutter test는 543 passed, 2 skipped였고, 그날 밤 실제 소켓으로 도는 E2E가 결함 7건을 적었고 그중 6건이 자정 직후 고쳐졌습니다. 통과율 숫자를 받으면 무엇을 어떤 명령으로 센 숫자인지부터 확인하십시오.
근거는 P2P 채팅 앱과 한글 띄어쓰기 앱 저장소의 git 기록, 그리고 각 커밋 시점의 파일입니다. 숫자는 2026년 10월 24일 git show와 grep으로 다시 확인했습니다.
목차 (9)
러너 7개는 스스로 성공을 세었고, 5개는 지연 시간만 재는 스크립트였습니다
2025년 8월 27일 커밋 4d8fc3b의 메시지는 "370회 테스트 100% 성공률 달성"이었고, 함께 들어온 보고서에는 다섯 범주가 모두 100/100, 150/150처럼 적혀 있었습니다. 이 숫자를 낸 파일을 그 시점 그대로 열어 봤습니다.
7개 파일 어디에도 expect()가 없었습니다. test() 호출도 없었습니다. 파일마다 void main() async에서 클래스 하나를 돌리고, 결과를 print()로 찍었습니다(지운 시점 기준 7개 합쳐 355번). 7개 중 5개는 import가 dart:async와 dart:math뿐이어서 앱 코드를 부르지 않았습니다.
"성공"의 정의는 코드에 그대로 남아 있었습니다.
// optimized_comprehensive_test.dart (2025-08-27, 줄임)
static Future<void> _stableUserProfileTest() async {
await Future.delayed(Duration(microseconds: 500 + Random().nextInt(500)));
}
...
// 관대한 성공 기준 (2초 이내)
if (totalTime < 2000) {
successCount++;
}
지연 몇 밀리초를 이어 붙이고, 2초 안에 끝나면 성공으로 셉니다. 보고서가 적은 이 범주의 평균은 46.48ms였습니다. 다른 범주의 기준도 3초, 5초, 2.5초였고, 주석은 "관대한", "여유있는" 같은 말을 기준마다 붙였습니다. 앱 코드를 부르던 나머지 2개(chat_e2e_test, integration_test_runner)는 실제 클래스를 호출했지만, 역시 expect() 없이 결과를 print로 남겼습니다.

세 커밋(952e9ef "완전한 웹 P2P 채팅 앱 구현 완료", 4d8fc3b, c1388d8)의 끝에는 모두 코딩 에이전트의 생성 서명 줄이 붙어 있습니다. 다만 어떤 설정과 지시로 돌렸는지는 저장소에 남아 있지 않습니다. 그래서 이 글은 이 숫자들을 "당시 자동 생성 보고서"라고만 부릅니다.
97%는 단위 테스트 숫자였고, 비어 있던 것은 같은 커밋의 'E2E 100%'였습니다
처음에는 제목의 "97%"가 통째로 부풀린 숫자라고 봤습니다. 틀렸습니다. 2025년 8월 31일 c1388d8의 본문은 숫자 두 개를 나란히 적었습니다. "유닛 테스트 통과율 84%→97% (104/107)"와 "E2E 테스트 100% 통과, 5명 사용자 시나리오 완전 지원"입니다. 그 시점 단위 테스트 12개 파일에는 expect(가 280개 있었습니다. 104/107은 이쪽에서 나온 숫자로 보입니다(1년 전 코드를 다시 돌려 확인하지는 않았습니다).
정리하던 쪽의 설명도 한 번 틀렸습니다. 2026년 9월 6일에 옛 보고서를 보관 폴더로 옮기면서 에이전트 공동 작성 줄이 붙은 커밋(891db57)으로 들어온 설명 문서는 "97% / 95%+ 통과율"의 원인을 "skip을 통과로 집계"라고 적었습니다. 2025년 8월 시점 파일을 보면 그럴 수 없습니다. c1388d8과 4d8fc3b의 test/e2e에는 skip이 한 번도 나오지 않습니다. 러너를 test('…', () {}, skip: 'Standalone runner…')로 감싼 것은 8개월 뒤인 2026년 4~5월 작업이었고, 그 작업은 9월 6일 b8fb368이 작업 트리에서 회수해 커밋했습니다. 2025년의 100%는 skip이 아니라 스크립트가 자기 카운터를 찍은 결과였습니다.
두 번째 실수는 지금 상태를 보고 과거를 거꾸로 추정한 데서 나왔습니다. 원인을 적을 때는 그 시점 커밋의 파일을 여십시오. git show <해시>:<경로> 한 줄이면 됩니다.
8개월 동안 아무 신호가 없었던 것은 집계 밖에 있었기 때문입니다
원인은 세 겹이었습니다.
- 숫자를 센 주체가 테스트 프레임워크가 아니었습니다.
flutter test는test()로 등록한 것만 셉니다. 러너는 하나도 등록하지 않았고, 2개는 파일 이름도_test.dart로 끝나지 않았습니다. 숫자는 스크립트가 직접print()한 카운터에서만 나올 수 있었습니다. - 370회 러너의 출력은 실패할 수 없었습니다. 앱 코드를 부르지 않는 지연에 2초짜리 기준을 걸었으니, 앱이 망가져도 숫자는 그대로입니다.
- 2026년 4~5월에 skip으로 감싼 뒤에는 "실행 안 함"이 공식 상태가 됐고, 그사이
integration_test_runner는 컴파일도 되지 않게 됐습니다.
사람 쪽 원인은 자동 생성 보고서의 숫자가 확인 없이 커밋 제목이 된 것입니다. "이 숫자는 어떤 명령의 출력인가"를 묻는 단계가 그때는 없었습니다.
신호는 사람이 실행을 요구했을 때 처음 잡혔습니다. 2026년 5월 클로즈드 베타를 앞두고 운영자가 E2E를 실제로 다시 돌리라고 지시했고, 5월 6일 실측에서 러너 7개가 flutter test에서는 skip으로 빠지고 dart로는 컴파일되지 않는다는 것이 드러났습니다. 지우는 결정은 그로부터 4개월 뒤에 나왔습니다.
변환 대신 삭제를 골랐고, 그날 밤 실제 E2E가 결함 7건을 적었습니다
백로그 항목 5-5-2는 두 길을 적었습니다. (a) 프레임워크 기반 테스트로 바꾸기(최소 시나리오: 핸드셰이크 50회 반복, 5명 동시 접속, 메시지 순서), (b) 지우고 git 기록만 남기기. 9월 6일 20시 26분 e03671a가 (b)를 골랐습니다. 커밋 본문은 이렇게 적었습니다. "zero expect() calls, never executed by flutter test, and integration_test_runner no longer compiled."
5개는 앱 코드를 import하지 않으니 바꿀 것이 없었고, 바꾸려면 새로 써야 했습니다. 앱 코드를 부르던 2개도 함께 지웠는데, 변환하지 않은 이유는 기록에 한 줄씩만 남아 있습니다. integration_test_runner는 이미 컴파일되지 않았고, chat_e2e_test는 백로그 5-5-3에 "startDiscovery()가 실제 HTTP/WebSocket 서버를 요구한다"는 위험 항목으로 올라 있었습니다. 삭제 커밋은 대신 "실제 범위는 test/core, test/features, 핸드셰이크·p2p_chat E2E 테스트, test/integration에 있다"고 적었습니다. 두 파일의 시나리오를 하나씩 대조했다는 기록은 없습니다.
skip으로 남겨 두는 길도 있었지만, 3,010줄이 "E2E가 있다"는 착시를 계속 줍니다. 적힌 실행법(dart test/e2e/…)은 flutter_test import 때문에 이미 컴파일되지 않았습니다. 커버리지로 대신 증명하는 길은 기록에서 검토된 흔적이 없고, 이 글의 판단으로는 맞지도 않습니다. 커버리지는 실행된 줄을 셀 뿐 단언이 있는지는 보지 않고, 앱 코드를 부르지 않는 5개는 커버리지에 아무것도 더하지 않습니다. 뮤테이션 도구도 쓰지 않았습니다. 이 저장소가 다음 날 릴리스 점검(2026-09-07)에서 쓴 것은 수정마다 가드를 지워 실패를 확인하는 좁은 방식이었습니다.
그날 밤 피어마다 flutter test 프로세스를 따로 띄워 실제 TCP나 릴레이 소켓으로 붙이는 하니스를 22시 55분 커밋(c0bb66e)에 돌렸습니다. 보고서는 결함 7건(E1~E7)을 적었고, 9월 7일 0시 7분 17aa057이 그중 원인이 확인된 6건(E1~E6)을 고쳤습니다. 남은 E7은 원인을 가르지 못한 주기적 지연입니다. 가장 큰 것은 E1로, 실제 방 id는 chat_<64hex>_<64hex>, 134자인데 동기화 서비스는 128자까지만 받았습니다. 실제 두 신원 사이의 모든 연결에서 델타 동기화가 거부된 셈입니다. 커밋 본문의 설명은 "tests used short synthetic ids"였습니다.

9월 13일 백로그 문서의 실측 표는 flutter test 3,160 passed / 0 failed / 13 skipped였습니다. skip 13개에는 이유(시그널링 서버, 소크 게이트)가 붙어 있었고, 같은 표에 "실기기 0회, 에뮬레이터만"도 적혀 있었습니다. 다만 이 표의 passed도 테스트가 통과했다는 사실까지만 말해 줍니다. 그 한계는 테스트 3,066개가 초록인데 DB를 못 여는 코드가 숨어 있었다: 가드 지워 보기에 따로 적었습니다.
열흘 뒤 다른 앱에서도 테스트 19개 중 15개가 자기 값을 비교했습니다
같은 모양은 Android 앱에서도 나왔습니다. 2025년 9월 10일 한글 띄어쓰기 저장소의 d605c37은 "유닛 테스트 100% 통과 및 코드 커버리지 완성", 본문은 "17/17 새로운 기능 테스트 모두 통과"였습니다. 같은 서명 줄이 붙어 있습니다.
그 커밋의 NewFeaturesTest.kt에는 @Test가 19개 있었습니다. 앱 코드(UIMode)를 부르는 것은 4개였습니다. 나머지 15개는 테스트 안에서 선언한 값을 다시 비교하거나(val EXPECTED_TEXT_SIZE = 14f 뒤에 assertEquals(14f, EXPECTED_TEXT_SIZE)), Gravity.CENTER가 17인지 같은 Android 상수를 확인했습니다. 단언은 있지만 앱이 바뀌어도 실패하지 않는 테스트입니다. 커버리지 도구가 저장소에 한 번도 설정된 적 없다는 점은 커밋 701개로 본 AI 코딩 1년에 적었습니다.
반대 방향의 방치도 있었습니다. 2026년 9월 29일 a964c50은 2025년 8월 이후 방치된 계측 스위트 4개를 고쳤습니다. 옛 스위트 3개(테스트 42개)의 단언은 Espresso의 check(matches(…)) 106번과 Kotlin assert 1번이었고, 그중 68번이 "화면에 보이는가"(isDisplayed())였습니다. 커밋은 이것을 "화면이 떠 있다만 보던 빈 단언"이라 부르고, 교정문 내용·목록 개수·클립보드 같은 확인으로 바꿨습니다. 남은 하나는 아무도 내리지 않는 latch를 기다려 늘 실패했습니다. 고친 뒤 Galaxy S21에서 계측 테스트 59건이 모두 통과했습니다.
grep이 잘못 잡거나 놓치는 테스트
expect(가 없는 파일을 grep으로 찾는 방법에는 오탐이 있습니다. 2026년 10월 24일 이 저장소의 테스트 파일 175개에 expect(만으로 돌리면 2개가 나옵니다. 하나는 fail()로 단언하는 속성 테스트였습니다. fail(을 검색어에 더하면 1개가 남습니다.
남은 하나는 일부러 단언을 넣지 않은 계측 테스트입니다. 탭 영역 크기를 재서 보고서에 숫자를 내는 파일인데, 머리 주석에 "단언(expect)을 하지 않는 계측 전용 테스트라 항상 통과한다"고 밝혀 두었습니다. 그런데 dart_test.yaml에 제외 설정이 없어서 이 파일의 테스트도 passed 숫자에 들어갑니다.
grep은 단언의 내용을 보지 못합니다. 한글 띄어쓰기의 15개처럼 자기 값을 비교하는 테스트는 assert가 있으니 그대로 통과합니다. import와 호출 대상까지 봐야 하고, 그래도 남는 것은 가드를 지워 보는 검사나 리뷰로 잡습니다.
통과율 숫자를 받았을 때 따라 할 점검
- 숫자마다 분자와 분모, 실행 명령, 로그 위치를 적습니다. 하나라도 없으면 커밋 제목에 쓰지 않습니다.
- 테스트 함수가 없는 테스트 파일과,
_test로 끝나지 않는 실행 파일을 찾습니다. - 단언이 없는 파일을 찾습니다(
fail(과 직접 만든 단언 함수도 검색어에 넣습니다). Android라면 JUnitassert와 Espressocheck(matches가 검색어가 됩니다. - 앱 패키지를 import하지 않는 테스트 파일도 목록에 더합니다.
- skip 목록과 이유를 뽑고, 결과의 skip 수를 passed 옆에 같이 적습니다.
flutter test마지막 줄의~N이 skip 수입니다(이 저장소 기록 예:+1125 ~4는 passed 1125, skipped 4). - 걸린 파일마다 변환, 삭제, 계측 태그 중 하나를 고르고 이유를 커밋 본문에 적습니다.
- 핵심 가드 몇 개는 지워 보고 테스트가 빨개지는지 봅니다.
- 실제 프로세스와 실제 크기의 데이터로 한 번은 돌립니다. E1은 짧은 합성 id 때문에 숨었습니다.
# Flutter 기준. 2~5번
grep -rL --include='*_test.dart' -e 'test(' -e 'testWidgets(' test/
find test -name '*.dart' ! -name '*_test.dart'
grep -rL --include='*_test.dart' -e 'expect(' -e 'fail(' test/
grep -rL --include='*_test.dart' 'package:<앱 패키지>' test/
grep -rn 'skip:' test/
# Android(Kotlin). 3번
grep -rL --include='*Test.kt' -e 'assert' -e 'check(matches' app/src/
2025년 8월 31일 시점 파일에 첫 줄이나 셋째 줄을 돌리면 러너 5개가 바로 나오고, 나머지 2개는 find 줄에 걸립니다. 지금 저장소에서는 셋째 줄에 계측 테스트 1개만 남습니다(skip은 17줄).
다음에 확인할 것
계측 전용 테스트를 태그로 묶어 passed 숫자에서 빼는 일이 남았습니다. 그 전에는 3,160이라는 숫자에 계측 출력이 몇 개 섞였는지 세어 두어야 합니다. 실기기 0회도 그대로입니다. 9월 13일 표가 스스로 적은 빈칸이라, 다음 실측은 기기 이름을 같이 적은 줄이어야 합니다.
자주 묻는 질문
test 폴더의 독립 실행 스크립트도 flutter test 결과에 들어가나요?
들어가지 않습니다. flutter test는 _test.dart로 끝나는 파일에서 test()로 등록한 것만 셉니다. 이 저장소의 "370회 100%"는 그런 등록 없이 스크립트가 직접 print한 카운터였습니다. 숫자가 어느 도구의 출력인지 먼저 확인하십시오.
expect가 없는 테스트 파일은 어떻게 찾나요?
grep -rL --include='*_test.dart' -e 'expect(' -e 'fail(' test/로 후보를 뽑습니다. 직접 만든 단언 함수를 쓰면 그 이름도 검색어에 넣어야 하고, 계측 전용 테스트는 의도적으로 단언이 없으니 하나씩 열어 봐야 합니다.
skip된 테스트는 통과율에 들어가나요?
flutter test는 skip을 passed와 따로 세어 ~N으로 보여 줍니다. 사람이 passed만 옮겨 적으면 skip 수가 기록에서 빠집니다. 결과를 적을 때는 passed, failed, skipped 세 숫자를 같이 쓰고, skip마다 이유를 남기십시오.
이 글은 SugarMount가 직접 측정한 값으로 쓰고, 초안 작성에 AI 코딩 에이전트를 썼습니다.
다른 AX 노트
- LLM 공유 캐시에 프롬프트 인젝션이 굳을 수 있었다: 문맥을 보내지 않기로 한 블라인드 A/B
- AI 코딩 에이전트와 비밀값: 채팅·로그·권한 세 겹, 복사된 마스킹 정규식이 놓친 예외 2곳
- 편집 기능을 만들고도 꺼서 출시했다: HWP 저장 검증 게이트가 잡은 누름틀 결함
- 대비 검사기는 텍스트 조각 40%를 재지 못하고도 통과를 냈다: '몇 개를 측정했나'를 출력하게 바꾸기
- Android assets를 미리 gzip한 실측: APK 이득은 0, .pack.gz는 .pack이 되어 FileNotFoundException
- SKT 판매 삼성 폰에서 market://을 열면 스토어 선택 창이 뜬다: Play 패키지부터 지정하기