← Posts

Testing · Coverage · Behavior

무엇을 테스트하고 무엇은 안 하나

다 테스트하려다 다 못 한다. 구현이 아니라 행동을, 정상 경로가 아니라 경계를 노린다. 커버리지 숫자의 함정과 깨지기 쉬운 테스트, 그리고 테스트하지 않아도 되는 것까지.

목차
  1. 다 테스트하려다 다 못 한다
  2. 구현이 아니라 행동을 테스트한다
  3. 경계와 엣지에 버그가 산다
  4. 커버리지 숫자는 함정이다
  5. 깨지기 쉬운 테스트는 짐이 된다
  6. 테스트하지 않아도 되는 것
  7. 실무에서: 신뢰가 목표지 숫자가 아니다
  8. 정리

테스트는 공짜가 아니라서, “다 짜자”는 답이 아니다. 값이 큰 걸 골라야 한다. 무엇을 노리고 무엇은 놓아줄지가 이 글이다.

다 테스트하려다 다 못 한다

모든 코드, 모든 경우를 테스트하려 들면 테스트 코드가 제품 코드보다 커지고, 유지가 안 돼 결국 통째로 방치된다. 시간은 유한하니 어디에 쓸지를 골라야 한다.

기준은 하나로 요약된다 - 틀렸을 때 아픈 곳부터. 핵심 비즈니스 규칙, 자주 바뀌는 코드, 잘못되면 돈·신뢰가 나가는 곳. 나머지는 뒤로 미룬다.

구현이 아니라 행동을 테스트한다

가장 중요한 원칙이다. 테스트는 **“무엇을 하는가”(행동)**를 확인해야지 **“어떻게 하는가”(구현)**에 붙으면 안 된다.

plaintext
좋음:  add(2, 3)이 5를 반환한다        (행동: 입력→출력)
나쁨:  add가 내부에서 sum 변수를 쓴다   (구현: 어떻게 하는지)

구현에 붙은 테스트는 로직이 그대로인데 내부만 다듬어도 깨진다. 그러면 리팩터링할 때마다 테스트를 고쳐야 하니, 안전망이 아니라 족쇄가 된다. 행동만 보면 내부를 자유롭게 바꿔도 초록불이 유지된다 - 겁 없이 바꾸는 그 값이 여기서 지켜진다.

경계와 엣지에 버그가 산다

정상 경로(“2+3=5”)만 테스트하면 정작 버그는 못 잡는다. 버그는 경계에 산다.

  • 0, 빈 값, null - 빈 목록, 빈 문자열, 없는 값.
  • 최소·최대·넘침 - 정수 한계, 아주 긴 입력.
  • 음수·경계값 - 잔액이 정확히 0일 때, 딱 한도일 때.
plaintext
출금 테스트:  1000원 출금 (정상)     ← 이것만 짜기 쉽다
             잔액과 같은 금액 (경계) ← 여기서 잘 샌다
             잔액보다 1원 많이 (경계 밖)
             음수 금액 (엣지)

정상 경로는 대개 잘 돌아간다. 틀리는 건 늘 구석이라, 테스트도 구석을 노려야 값이 있다.

커버리지 숫자는 함정이다

“테스트 커버리지 100%“가 목표처럼 쓰이지만, 이 숫자는 속기 쉽다. 커버리지는 “테스트가 그 줄을 실행했나”만 잰다. 검증했나가 아니라.

plaintext
test("주문을 생성한다") {
  createOrder(...)     // 함수는 실행됨 → 커버리지 오름
}                      // 그런데 결과를 아무것도 확인 안 함(assert 없음)

이 테스트는 커버리지를 올리지만 아무것도 보장하지 않는다. 함수가 도중에 틀린 값을 내도 통과한다. 그래서 “100% 커버리지”가 “100% 안전”이 아니다 - 실행 ≠ 검증. 숫자를 좇으면 assert 없는 껍데기 테스트만 늘어난다.

깨지기 쉬운 테스트는 짐이 된다

사소한 변경에 우수수 깨지는 테스트(brittle test)는 없느니만 못할 때가 있다. 로직은 멀쩡한데 빨간 줄이 뜨면, 사람은 곧 “또 그 테스트네” 하고 무시하거나 지운다. 안전망에 구멍이 나는 것이다.

깨지기 쉬운 원인은 대개 둘 - 구현에 붙었거나(위의 행동 vs 구현), 목을 과하게 썼거나. 좋은 테스트는 로직이 바뀔 때만 깨지고, 리팩터링엔 조용하다.

테스트하지 않아도 되는 것

반대로 굳이 안 짜도 되는 것도 있다.

  • 남의 코드 - 프레임워크·라이브러리는 그쪽이 이미 테스트했다. List.add가 되는지 우리가 검증할 필요는 없다.
  • 자명한 코드 - 로직 없는 단순 getter/setter, 그냥 넘겨주기만 하는 위임.
  • 거의 안 바뀌고 틀려도 싼 것 - 값이 낮은 곳에 시간을 쓰면 정작 중요한 데를 못 짠다.

“커버리지를 채우려고” 이런 걸 테스트하면 숫자만 오르고 값은 없다.

실무에서: 신뢰가 목표지 숫자가 아니다

  • **“이거 믿고 배포하나”**를 기준으로 삼는다. 커버리지 90%여도 핵심 규칙이 안 잡혔으면 부족하고, 70%여도 중요한 게 다 잡혔으면 충분하다.
  • 버그가 났던 자리엔 테스트를 남긴다 - 한 번 틀린 곳은 또 틀린다. 고치면서 그 케이스를 테스트로 박아 회귀를 막는다.
  • 커버리지는 참고용 지표로만 - 낮은 곳을 찾는 데는 쓰되, 목표 숫자로 삼지 않는다.

정리

  • 다 테스트할 순 없으니 틀렸을 때 아픈 곳부터 고른다.
  • 구현이 아니라 행동을 테스트한다 - 그래야 리팩터링에 안 깨지는 안전망이 된다.
  • 버그는 경계·엣지에 사니 정상 경로만 짜면 놓친다. 커버리지 100%는 실행일 뿐 검증이 아니다.
  • 깨지기 쉬운 테스트는 짐이고, 남의 코드·자명한 코드는 안 짜도 된다. 목표는 숫자가 아니라 신뢰다.

다음 글은 아래층 바깥 - 진짜를 붙여 이음매를 보는 통합과 E2E로 간다.