테스트를 분명히 썼는데, 애초에 수집되지 않고 있었다
블로그 댓글 컴포넌트에 회귀 테스트를 새로 붙이는 작업이었다. 이 파일은 자동 테스트가 0건인 상태로 두 번 수정됐고, 그중 한 번이 실제 회귀를 냈다. 게스트가 방금 작성한 자기 댓글의 삭제 버튼이 사라지는 문제였는데, 당시엔 코드 리뷰가 우연히 잡아서 넘어갔다. 테스트가 없는 자리에서는 리뷰가 유일한 방어선이었다는 뜻이고, 리뷰가 매번 그걸 잡아준다는 보장은 없다.
그래서 이번엔 제대로 테스트를 붙이기로 했다. 그런데 정작 이 작업에서 가장 중요한 발견은 새로 쓴 테스트의 내용이 아니라, 그 테스트들이 돌고 있다고 믿었지만 실제로는 한 번도 실행되지 않고 있었다는 사실 쪽이었다. 테스트 하나하나를 아무리 꼼꼼히 설계해도, 그 테스트가 애초에 실행 목록에 올라 있지 않으면 전부 무의미하다. 이번 작업은 그 사실을 직접 겪고서야 체감했다.
회귀가 났던 자리에 테스트 7개를 추가하다
두 가지를 겨냥해 테스트 7개를 추가했다. 하나는 삭제 권한 UI의 가시성 — 누구에게 삭제 버튼이 보여야 하는가 — 이고, 다른 하나는 댓글을 등록한 뒤 목록이 실제로 재조회되는가다. 앞서 났던 회귀가 정확히 이 두 지점에서 발생했기 때문에, 새 테스트도 그 두 지점을 그대로 겨냥해서 썼다. 자동 테스트가 없던 컴포넌트에 처음 테스트를 붙일 때는 무엇부터 커버할지 고르기가 애매한데, 이번엔 그 선택이 쉬웠다. 실제로 사고가 났던 지점부터 메우면 됐기 때문이다.
뮤테이션으로 하중을 확인하다
테스트를 쓰는 것과 그 테스트가 실제로 뭔가를 지키는 것은 다르다는 걸 이미 겪어봤기 때문에, 이번에도 하나씩 뮤테이션으로 확인했다. 삭제 조건을 항상 참으로 바꾸면 1건이 빨간불이 됐고, 항상 거짓으로 바꾸면 2건이 빨간불이 됐다. 댓글 등록 후 재조회 로직을 제거하면 1건이 빨간불이 됐다. 일곱 개 모두 최소 하나 이상의 뮤테이션에 반응한다는 걸 확인하고 나서야 이 테스트들이 실제로 하중을 가지고 있다고 판단했다. 통과하는 테스트가 아무것도 증명하지 않을 수 있다는 걸 며칠 전 다른 사례들로 이미 겪은 뒤였기 때문에, 이번엔 뮤테이션 확인을 건너뛰지 않았다.
진짜 발견 — include 화이트리스트
여기까지는 예상한 흐름대로였다. 진짜 발견은 그 다음이었다. 테스트를 다 쓰고 전체 스위트를 돌렸는데, 결과로 나온 개수가 예상보다 적었다. 방금 추가한 7건이 총계에 제대로 반영되지 않은 것 같았다. 처음엔 파일 경로를 잘못 지정했나, 테스트 함수 이름에 오타가 있나 하는 쪽부터 의심했다. 하나씩 지워가며 원인을 좁혀보니 코드 쪽 문제가 아니었다. 설정 파일을 열어보니 vitest의 include가 화이트리스트 형태였고, 방금 테스트를 넣은 디렉터리가 그 목록에 없었다.
include: ['a/**/*.test.ts', 'b/**/*.test.ts', ...]
이 형태에서는 목록에 없는 디렉터리의 테스트 파일은 실패하는 게 아니라 애초에 수집 대상에서 빠진다. 실행도, 스킵도, 아무것도 되지 않는다. 에러도 뜨지 않고 경고도 없다. 그냥 처음부터 그 파일이 없었던 것처럼 조용히 넘어간다. 방금 뮤테이션까지 돌려서 하중을 확인한 테스트 7개가, 정작 평소 CI에서는 단 한 번도 실행되지 않고 있었던 셈이다.
모노레포처럼 테스트 디렉터리가 여러 개로 나뉘어 있는 구조에서는 이 함정이 특히 잘 숨는다. 디렉터리마다 describe 블록이 있고 파일 이름도 관례를 따르기 때문에, 겉으로 봐서는 이 파일이 스위트에 포함되는지 아닌지 전혀 구분이 안 간다. 파일을 열어보고, 함수를 읽어보고, 심지어 로컬에서 그 파일 하나만 지정해서 돌려보면 멀쩡히 통과한다. 문제는 전체 스위트 — 실제로 CI가 돌리는 그 커맨드 — 기준으로 봤을 때만 드러난다는 점이다.
삼켜진 테스트가 더 있었나 확인하다
이 사실을 확인하자마자 걱정된 건 "그동안 이렇게 조용히 삼켜진 테스트가 또 있는 게 아닐까"였다. include 패턴 하나를 빠뜨리는 실수는 언제든 반복될 수 있는 종류였기 때문이다. 새 디렉터리를 만들 때마다 이 실수가 일어날 수 있다면, 지금까지 쌓인 테스트 중에서도 같은 이유로 조용히 빠진 게 있을 가능성을 배제할 수 없었다.
그래서 전체 테스트 파일을 하나하나 열거해서 include 목록과 대조하는 방식으로 확인했다. 파일이 몇 개 없는 상태라 손으로 대조하는 게 가능했지만, 이 방식이 계속 갈 방법은 아니라는 것도 동시에 느꼈다. 테스트 디렉터리가 늘어날수록 사람이 매번 목록을 대조하는 방식은 결국 놓치게 되어 있다. 결과적으로 다행히 그동안 조용히 누락된 기존 테스트는 없었다. 그 디렉터리에는 이번이 처음 추가되는 테스트 파일이었기 때문에, 지금까지는 삼켜질 대상 자체가 없었던 것이다. 하지만 순서가 조금만 달랐어도 — include를 먼저 확인하지 않고 넘어갔다면 — 이번에 새로 쓴 7개는 통과 표시도 실패 표시도 없이 그대로 삼켜질 뻔했다. include에 해당 디렉터리 패턴을 추가하고 나서야 7개 모두 실제로 수집되고 실행되기 시작했다.
확인 방법 자체는 특별할 게 없었다. include 패턴이 커버하는 디렉터리 목록과, 실제로 테스트 파일이 존재하는 디렉터리 목록을 나란히 놓고 하나씩 대조했을 뿐이다. 다만 이 대조를 자동화된 도구 없이 손으로 했다는 점은 아쉬운 부분으로 남았다. include 목록에 없는 디렉터리에 테스트 파일이 생기면 경고를 내는 식의 안전장치가 있었다면 애초에 이런 확인 작업 자체가 필요 없었을 것이다.
교훈
- 화이트리스트 설정은 조용히 실패한다. 블랙리스트(제외 목록)는 빠뜨리면 불필요한 게 도는 정도로 끝나지만, 화이트리스트는 빠뜨리면 필요한 게 안 돈다. 그리고 안 도는 건 아무 소리도 내지 않는다.
- 새 디렉터리에 첫 테스트를 추가할 때는 수집 설정을 함께 확인해야 한다. 테스트를 썼는데 안 도는 상태는 테스트가 아예 없는 것보다 나쁘다. 지켜지고 있다고 믿게 만들기 때문이다.
- 테스트 개수를 작업 전후로 세어보는 습관이 이걸 잡았다. 7개를 추가했는데 총계가 7 늘지 않았다면, 그 자리에서 멈추고 원인을 확인해야 한다.
- 테스트 자체를 아무리 잘 써도, 그 테스트를 실행 대상에 포함시키는 설정이 틀리면 아무 의미가 없다. 테스트 코드의 품질과 별개로 수집 설정도 검증 대상이라는 걸 이번에 다시 배웠다.
- 당장은 손으로 대조해서 넘어갔지만, 디렉터리가 늘어나면 같은 방식으로는 못 버틴다. include를 화이트리스트로 유지할 거라면 새 디렉터리를 추가하는 절차 자체에 이 확인을 끼워 넣어야 한다.