테스트가 green인데 화면은 비어 있었다

증상

주차별 학습 진행률을 보여주는 막대 차트가 있다. 스크린샷을 찍어 눈으로 확인하다가, 신규 계정에서 그 자리가 빈 상자로 보이는 것을 발견했다. 축도 있고 눈금도 있는데 막대가 없다.

테스트는 전부 통과하고 있었다. 그 화면을 다루는 테스트도 있었고, 통과였다.

왜 통과했나

신규 사용자는 아직 학습 기록이 없으니 모든 주의 값이 0이다. 차트는 그 0을 받아 막대 높이를 0으로 계산했다. 화면에는 높이 0짜리 막대 일곱 개가 그려졌다. 정확히 말하면 그려지지 않았다.

테스트는 이렇게 생겼다. 데이터를 넣고, 만들어진 막대의 좌표를 읽고, 기대값과 비교한다. 신규 사용자 시나리오에서는 기대값도 0이었다. 코드가 0을 만들었고 테스트가 0을 기대했으니 통과다.

테스트는 틀리지 않았다. 코드도 시킨 대로 했다. 그런데 사용자는 고장난 화면을 본다.

「나란히 선 단언」

이 구조에는 이름을 붙일 만한 특징이 있다. 테스트가 화면과 같은 값을 나란히 적어 두는 형태라는 것이다.

화면 코드가 높이 = 값 × 배율을 계산하고, 테스트가 expect(높이).toBe(값 × 배율)을 단언하면, 둘은 같은 규칙을 두 번 쓴 것뿐이다. 규칙 자체가 틀렸을 때 양쪽이 똑같이 틀리므로 테스트는 계속 통과한다.

실제로 확인해 봤다. 화면 쪽 값을 일부러 퇴행시키고 테스트를 돌렸더니, 다섯 번 중 다섯 번 통과했다. 잡으라고 만든 테스트가 한 번도 잡지 못했다.

같은 계열이 네 번 나왔다

혼자 있는 사건이 아니었다. 화면 작업을 세 단계에 걸쳐 진행하는 동안 같은 모양이 네 번 반복됐다.

한 번은 텍스트 줄임 처리를 검증하는 테스트가, 실제로는 줄임이 일어나지 않는 짧은 문자열을 쓰고 있었다. 결함을 알면서 그 조건을 피해 가는 입력으로 통과 중이었던 셈이다.

또 한 번은 테스트 환경의 레이아웃 제약이 실제 화면과 달라서, 계획이 지시한 대로 테스트를 짰는데 결함이 재현되지 않았다. 수정 전 코드로도 그 테스트는 통과했다. 실제 화면에서는 요소가 240픽셀 넘게 어긋나 있었는데도.

그래서 무엇을 바꿨나

두 가지를 했다.

하나는 단언의 성격을 바꾼 것이다. 계산 결과를 그대로 베끼는 대신, 성질을 단언한다. "값이 0이 아닌 데이터가 하나라도 있으면 눈에 보이는 높이의 막대가 존재한다" 같은 형태다. 화면이 규칙째로 퇴행해도 이건 깨진다.

다른 하나는 조금 무식한 방법이다. 어떤 규칙은 소스 텍스트를 직접 검사한다. 특정 값이 하드코딩으로 되돌아오는 것을 막는 가드다. 우아하지 않지만, 우아한 테스트가 다섯 번 다 놓친 퇴행을 이건 잡는다.

육안 확인은 대체되지 않는다

이 사건들의 공통점은 전부 스크린샷을 눈으로 보다가 발견됐다는 것이다. 300건이 넘는 테스트가 green인 상태에서 그랬다.

테스트는 "내가 확인하라고 시킨 것"만 확인한다. 화면이 비어 있는지는 아무도 시키지 않았다. 자동화된 검증을 아무리 쌓아도, 실제로 렌더된 결과를 한 번 보는 일을 대신하지는 못한다.

지금은 화면을 건드리는 작업마다 릴리스 빌드로 캡처를 남기고, 통과한 테스트 수와 무관하게 그것을 본다.

← 개발 기록 목록으로