ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter 검증은 exit 0보다 산출물 확인이 먼저입니다
    AI Agent/flutter 2026. 7. 9. 21:00
    728x90
    반응형

    흐릿한 초록 체크가 뜬 모니터 옆에 펼쳐진 빈 종이 폴더
    CI는 초록불, 산출물은 빈 깡통

    CI가 초록불이라 그날은 마음 편히 퇴근할 뻔했습니다.
    골든 테스트가 전부 통과했고 exit 0이 떨어졌으니까요.
    화면 캡처를 자동으로 뽑는 파이프라인이었는데, 로그 맨 아래 초록색 체크를 보고 저는 "오늘 치는 끝났다" 하고 노트북을 덮었습니다.
    그런데 다음 날 아침, 캡처한 PNG들을 확인하려고 폴더를 열었더니 파일은 있는데 죄다 빈 깡통이었습니다.
    크기가 몇 바이트짜리, 아무것도 안 그려진 이미지들이 줄줄이 있었습니다.
    이미 초록불을 믿고 반나절을 넘긴 뒤였습니다.

    초록불이 거짓말을 한 건 아니었다

    폴더를 열었을 때 처음 든 생각은 캡처 코드가 깨졌나였습니다.
    그래서 캡처를 만드는 위젯 테스트를 로컬에서 직접 돌려봤죠.
    근데 로컬에선 멀쩡한 PNG가 잘 나왔습니다.
    그럼 CI에서만 다른 건데, 하고 로그를 줄 단위로 훑다가 이상한 걸 봤습니다.
    통과했다는 테스트 목록에, 정작 캡처를 만드는 테스트 이름이 하나도 없었습니다.

    억울해서 CI 로그를 처음부터 다시 봤습니다.
    근데 CI는 거짓말을 한 게 아니더군요.
    테스트 프로세스는 정말로 성공했습니다.
    문제는 제가 "프로세스가 성공했다"를 "내가 검증하려던 게 검증됐다"로 넘겨짚은 거였습니다.
    그 둘은 생각보다 멀었습니다.

    두 개의 조용한 실패가 겹쳐 있었다

    원인을 파보니 두 가지가 겹쳐 있었습니다.
    하나는 캡처 파일을 저장하는 상대경로가 테스트 파일 위치를 기준으로 해석돼서, PNG가 제가 보던 폴더가 아니라 엉뚱한 임시 위치에 생기고 있었던 것.
    그래서 제가 연 폴더에는 예전에 실패하며 남긴 껍데기 파일만 남아 있었습니다.
    이건 상대경로를 아예 절대경로로 바꿔서, 캡처가 떨어질 위치를 테스트가 스스로 정하게 하는 걸로 막았습니다.
    어디서 실행하든 같은 폴더에 쌓이도록요.

    다른 하나는 정작 그 캡처를 만드는 테스트 몇 개가 실행 조건이 안 맞아 통째로 skip됐다는 것이었습니다.
    flutter test로 그 파일만 따로 다시 돌려보니 맨 아래에 전부 건너뛰었다는 표시가 떠 있었습니다.
    skip은 실패가 아니라서 exit 0이 그대로 나왔고, 저는 그 줄을 초록불에 묻혀 한 번도 제대로 안 봤던 겁니다.
    두 조용한 실패가 겹치니 로그는 완벽하게 초록이었습니다.
    둘 중 하나만 있었어도 어딘가 어긋난 티가 났을 텐데, 둘이 서로의 흔적을 지워준 셈이었습니다.

    exit code는 산출물을 보증하지 않는다

    그날 이후 제 검증 코드에는 한 줄이 늘 붙습니다.
    프로세스가 성공했는지 말고, 기대한 산출물이 진짜로 그 개수만큼 나왔는지 세는 줄입니다.

    final result = await Process.run('flutter', ['test', '--machine']);
    final events = parseMachineEvents(result.stdout as String);
    final ran = events.where((e) => e.type == 'testDone' && !e.skipped);
    expect(ran, isNotEmpty, reason: '테스트가 전부 skip되진 않았는지');
    expect(captureDir.listSync().whereType<File>().length, expectedCaptures);
    

    exit code만 봤으면 둘 다 못 잡았을 겁니다.
    전부 skip돼도 초록, 파일이 엉뚱한 데 생겨도 초록이니까요.
    그래서 "몇 개가 실제로 돌았나"와 "몇 개가 실제로 생겼나"를 코드로 직접 세게 했습니다.
    숫자로 못 박아두니, 다음부터는 skip이 늘거나 캡처가 하나라도 비면 테스트 쪽에서 먼저 빨간불이 났습니다.
    사실 이 두 줄은 대단할 것도 없습니다.
    근데 이게 있고 없고가, 초록불을 믿고 반나절을 날리느냐와 테스트가 애초에 빨개져서 몇 초 만에 알아채느냐를 갈랐습니다.

    그 뒤로 초록불을 볼 때마다 한 번씩 의심하게 됐는데, 얼마 안 가 비슷한 걸 또 찾았습니다.
    이번엔 빌드 스크립트였습니다.
    flutter analyze 결과를 파일로도 남기려고 파이프로 다른 명령에 넘겼는데, 그 바람에 최종 exit code가 analyze가 아니라 뒤쪽 명령 것으로 바뀌어 있었습니다.
    analyze가 경고를 잔뜩 뱉어도 스크립트는 태연히 0으로 끝났습니다.
    파이프 한 칸이 실패 신호를 통째로 삼킨 거죠.
    set -o pipefail 한 줄을 넣고 나서야 그 초록불이 정직해졌습니다.

    그 빈 PNG는 아직 폴더에 있다

    두 번 데이고 나니, 저는 어떤 초록불을 보든 "이 초록이 정확히 무엇을 성공이라고 부르고 있나"를 한 번 되묻게 됐습니다.
    전부 skip돼도, 파일이 비어도, 파이프가 신호를 삼켜도 초록은 똑같이 초록이니까요.

    그날의 빈 PNG 하나는 아직 지우지 않고 그 폴더에 그대로 뒀습니다.
    몇 바이트짜리, 열어보면 아무것도 없는 파일.
    초록불을 덜컥 믿고 싶어질 때 가끔 그걸 다시 열어봅니다.
    성공했다는 신호가 늘 성공을 뜻하지는 않는다는 걸, 그 빈 화면만큼 조용히 일러주는 게 없어서요.

    원문 기준

    원문 기준: https://docs.flutter.dev/testing

    원문 본 날짜: 2026-07-01.
    발행하기 전에 원문에서 바뀐 부분이 없는지만 짚고 넘어갑니다.

    728x90
    반응형
Designed by Tistory.