..

에러 테스트 범위

이번에 비어있던 테스트 코드를 작성을 하면서 마주친 질문이 있다.

“에러에 대한 테스트는 어디까지 작성해야 할까?”

특히 서비스 로직이 복잡해질수록 에러는 늘어나고, 그만큼 테스트 범위에 대한 기준이 없으면 테스트 코드가 쉽게 과해지거나, 반대로 의미 없어지기도 한다.

이번에 테스트 코드 작성 중 에러 검증 범위에 대한 논의가 필요했고, 몇 가지 선택지 중 하나를 정해야 했다. 이 글은 그 고민 과정과 최종적으로 정리한 에러 테스트 컨벤션을 정리해 보았다.


에러 테스트의 목적부터 다시 생각해보기

우선 에러를 던졌을 때 테스트의 목적을 명확히 할 필요가 있었다.

에러 테스트의 핵심 목적은 “의도한 위치에서 에러가 발생했는지”를 검증하는 것이다.

에러가 발생했다는 사실 자체보다,

  • 어디서
  • 어떤 이유로

에러가 발생했는지가 더 중요하다.

이 기준을 가지고 에러 테스트의 범위를 고민했다.

고려했던 세 가지 선택지

  1. 에러 타입만 검증
     await expect(fn()).rejects.toThrow(CustomError);
    
    • 장점
      • 가장 단순하고 작성 비용이 낮다.
      • 리팩토링에도 비교적 안전하다.
      • 실제로 한 함수에서 여러 에러 케이스를 던지는 경우가 적다.
    • 단점
      • 한 함수에서 같은 타입의 에러를 여러 곳에서 던질 경우
      • 정확히 어느 위치에서 발생했는지 알기 어렵다.
      • 결국 “에러가 났다”는 것만 확인하게 된다.
  2. 에러 메시지 or ERROR_CODE까지 검증
     await expect(fn()).rejects.toThrow('user not found');
     또는
     expect(error.errorCode).toBe(ERROR_CODE.USER_NOT_FOUND);
    
    • 장점
      • 에러 발생 위치를 구분할 수 있다.
      • 에러 메시지는 모든 에러가 기본적으로 가진 값이라 일관성을 유지하기 쉽다.
      • ERROR_CODE가 있다면 더 명확한 의도 표현이 가능하다.
    • 단점
      • 메시지 전체를 비교하면 테스트가 깨지기 쉬워질 수 있다.
  3. 에러 내부 객체 전체를 검증
    expect(error).toEqual({
        errorCode: ...,
        message: ...,
        data: ...
    });
    
    • 장점
      • 가장 명확하고 구체적인 검증
    • 단점
      • 대부분의 에러 객체는 로깅용 정보에 가깝다.
      • 서비스 로직에 직접적인 영향을 주지 않는 필드까지 검증하게 된다.
      • 테스트 오버헤드와 복잡성이 급격히 증가한다.

결론: 어디까지 검증할 것인가?

논의 끝에 아래와 같은 방향으로 정리했다.

  • 에러 타입만으로는 부족하다 -> 동일 타입의 에러가 여러 곳에서 발생할 수 있기 때문
  • 에러 객체 전체 검증은 과하다 -> 테스트 유지 비용이 높고 실질적인 가치가 낮다

ERROR_CODE가 있다면 ERROR_CODE를 검증한다

우리 서비스에서는 CustomError에 ERROR_CODE로 에러를 구분하고 있다.

expect(error.errorCode).toBe(ERROR_CODE.INVALID_TOKEN);

메시지보다 명확하고 문자열 변경에 덜 민감하다

ERROR_CODE가 없다면 메시지를 검증한다

단, 전체 일치가 아니라 포함 여부만 확인

await expect(fn()).rejects.toThrow(/token/i);

에러 메시지의 핵심 의미만 검증 전체를 검사하게 되면 같은 의미상의 문구도 테스트까지 수정해야하는 오버해드가 생긴다.

에러 수신 측에서 data를 사용한다면, 그때만 객체 검증을 추가한다

expect(error.data).toEqual({
  retryAfter: 30,
});

에러 객체의 data가 수신하는 측에서 비지니스 로직으로 사용이 된다면 검사하도록 추가하였다. 반환이 정확하게 되지 않았을 경우 문제가 생기는 경우에만.

마무리

테스트 코드의 범위를 정하는 일은 항상 어렵다고 느낀다. 어디까지 테스트해야 하는지, 어떤 코드까지 검증해야 하는지 매번 작성할 때마다 고민하게 된다.

테스트가 많고 자세하다고 해서 항상 좋은 것만은 아니라는 생각도 들었다. 오히려 중요한 건 이 테스트를 통해 무엇을 얻고 싶은지, 그리고 이 테스트가 어떤 가치를 주는지를 먼저 고민하는 것이라는 점을 다시 한 번 느끼게 되었다.

이번 논의를 통해 단순히 “테스트를 더 작성하자”가 아니라, 테스트의 목적과 유지 비용 사이의 균형을 생각해보는 계기가 되었던 것 같다.

비슷한 고민을 하고 있다면, 이 글에서 정리한 기준을 그대로 적용하기보다는 각 팀과 서비스의 특성에 맞게 참고하여 조정해보는 것도 좋은 방법일 것 같다.