툴과 개발 환경10 분 읽기

헤더의 초 단위 타이머가 무엇을 해도 테스트를 통과시켰습니다

제스처 배선은 순수 함수로 뺄 수 없어 픽셀 변화로 쟀습니다. 그 판정을 조용히 무의미하게 만드는 것이 셋 있었고, 셋 다 실제로 밟았습니다.

#verification#methodology#debugging#gotchas
개념 도식: 전체 화면 픽셀 비교에서 헤더 타이머만 변해도 통과 판정이 나오는 구조
글 내용을 요약한 개념 도식.

드래그로 칸을 칠하는 퍼즐 앱에서 제스처 배선을 검증해야 했습니다.

제스처 배선은 순수 함수로 뺄 수 없습니다. 붙이는 순서가 규칙이고, 틀리면 코어 상호작용이 죽습니다. 그래서 UI 테스트에서 판 영역의 픽셀이 몇 개 변했는지를 세기로 했습니다.

let cg = XCUIScreen.main.screenshot().image.cgImage!
let rect = CGRect(x: w*0.20, y: h*0.38, width: w*0.60, height: h*0.24)
cropped  CGContext(premultipliedLast)  RGBA 바이트 비교

질문 하나 드리겠습니다. 당신의 테스트가 통과할 때, 그 테스트가 실제로 무엇을 보고 있는지 증명할 수 있습니까?

1. 이 판정을 조용히 무의미하게 만드는 것이 셋 있었습니다

셋 다 실제로 밟았습니다.

① 전체 화면을 쓰면 안 됩니다

헤더에 초 단위 타이머가 있습니다. 그래서 **무엇을 해도 "변했다"**가 됩니다.

배선이 아예 없어도 통과하는 테스트가 됩니다. 판 영역만 잘라야 합니다.

② 정답에만 반응하는 입력으로 재면 날마다 뒤집힙니다

그 앱의 한 모드는 오답 칸에서 아무것도 남기지 않습니다. 실수 표시가 번쩍이고 사라집니다.

같은 좌표가 판 중앙에서는 616픽셀, 다른 칸에서는 0픽셀이었습니다. 항상 흔적이 남는 모드로 바꿔야 결정적이 됩니다.

③ "A가 B보다 크다"는 A=B=0일 때 헛돕니다

비교 전에 기준값 자체가 0이 아님을 따로 단정해야 합니다(tapChanged > 50).

이건 픽셀 판정만의 문제가 아닙니다. 비율·향상·차이를 재는 모든 판정이 분모가 0일 때 조용히 참이 됩니다.

2. 그리고 통과했다고 끝내지 않았습니다

배선 플래그를 한 줄 꺼서 다시 돌렸습니다. "탭 616 vs 드래그 0"으로 실패했습니다.

이 음성 대조를 하기 전까지는 그 테스트가 배선을 보고 있다는 증거가 없었습니다.

실제로 첫 판(전체 화면 비교)은 음성 대조에서도 통과해 버렸습니다. 그 시점에 ①을 발견했습니다. 음성 대조가 없었으면 ①을 영영 못 봤을 겁니다.

같은 배치의 0건을 실행하고 통과라고 찍힌 이야기빌드 세팅 하나가 61건을 죽인 이야기가 자매편입니다. 테스트는 통과했는데 버튼이 아무 일도 안 하던 이야기가 같은 계열의 앞글입니다.

3. 당신이라면 제스처 배선을 어떻게 검증하시겠습니까

  • (a) 콜백이 불렸는지 목킹으로 확인한다
  • (b) 접근성 액션으로 클릭해 결과 상태를 본다
  • (c) 화면 픽셀 변화를 센다

(a)는 배선이 아니라 콜백을 봅니다. 배선이 끊겨도 콜백을 직접 부르면 통과합니다.

(b)에는 함정이 있습니다. ⚠️ Compose의 performClick()은 의미 액션 경로와 구분되지 않습니다. 두 경로가 같은 콜백을 부르므로 "터치가 아래로 내려갔다"는 증거가 못 됩니다.

드래그로 재야 합니다. 의미 액션으로는 여러 칸을 칠할 수 없기 때문입니다. 실측:

오버레이에 .clickable { }  → 드래그가 칠한 칸 0개 (탭·롱프레스·드래그 전부 죽음)
semantics { onClick { } }  → 2개 이상            (터치가 캔버스로 내려간다)

그래서 답은 (c)입니다. 비싸지만 대안이 없습니다.

4. 접근성 배선도 같은 부류인데, 여기서는 더 이상했습니다

iOS에서 접근성 자식으로 선언한 뷰는 레이아웃을 못 받습니다. 라벨과 값은 트리에 들어가는데 프레임이 무한/0이라 읽히지만 누를 수 없습니다. 증상은 UI 테스트에서만 보입니다.

kAXErrorCannotComplete performing kAXScrollToVisibleAction

→ 오버레이 + 히트테스트 비활성으로 바꿉니다. 프레임을 받고 터치는 통과합니다.

⚠️ 시뮬레이터의 화면 낭독기는 시스템 명령으로 켜지지 않습니다. 설정해도 꺼진 값을 읽습니다. 그러니 접근성 코드를 "낭독기 켜짐"으로 감싸면 검증 자체가 불가능해집니다. 노드 수가 감당되면(225칸) 게이트를 빼고 검증 가능하게 두는 편이 낫습니다.

5. 픽셀 판정 체크리스트

1. 변할 수 있는 크롬(타이머·시계·배터리)을 잘라낸다 — 판 영역만
2. 입력은 상태와 무관하게 항상 흔적을 남기는 것으로 고른다
3. 기준값이 0이 아님을 먼저 단정한다
4. 배선을 꺼서 실패하는지 본다 (음성 대조)

6. 잡동사니 셋

  • 이 머신의 시뮬레이터는 한국어라 라벨로 요소를 찾으면 못 찾습니다. 런치 인자로 언어를 고정하고, 못 찾을 때는 계층 덤프를 출력합니다.
  • 목록에 오래된 기기가 섞여 있어 이름으로 지정하면 배포 타깃 불일치로 죽습니다. UDID로 지정하고 부팅을 먼저 기다립니다.
  • 결과 집계는 카운터 두 종류를 다 봐야 합니다.

한계

픽셀 판정은 비쌉니다. 스크린샷 · 크롭 · 바이트 비교가 들어가고 기기 · 해상도에 묶입니다. 그래도 제스처 배선에는 대안이 없었습니다. 이 글은 "픽셀 판정을 쓰세요"가 아니라 "쓸 거라면 이 넷을 지키세요"에 가깝습니다.

자가진단 3가지

  1. 당신의 UI 테스트가 비교하는 영역에 시계·타이머·배터리가 들어갑니까? 들어가면 그 테스트는 항상 "변했다"입니다.
  2. "A > B" 판정에서 A와 B가 둘 다 0일 수 있습니까? 기준값을 따로 단정하십시오.
  3. 고친 부분을 되돌려 그 테스트가 실패하는 걸 본 적 있습니까? 없으면 그 테스트가 무엇을 보는지 아직 모릅니다.

솔직한 부분

제일 무서운 건 ①이었습니다. 그 테스트는 통과했고, 음성 대조에서도 통과했습니다. 두 번 초록이었습니다. 배선을 지운 상태에서도요.

테스트가 초록인 것과 테스트가 무언가를 보고 있는 것은 다른 사실인데, 초록이 두 번 뜨면 그 구분이 흐려집니다. 제가 ①을 발견한 건 통찰이 아니라 "왜 꺼도 통과하지?"라는 당황이었습니다.

지금 딱 하나만 해 보세요. 최근에 쓴 테스트 하나를 골라, 그게 검증하는 코드를 한 줄 지우고 돌려 보십시오. 그대로 통과하면 그 테스트는 지금 아무것도 지키고 있지 않습니다.

관련 글