Jest에서 예외 타입 테스트하는 방법 — toThrow로 TypeError/message 검증
JavaScript 함수가 특정 예외를 던지는지, 그리고 그 예외의 타입(TypeError, ReferenceError 등) 까지 맞는지 테스트하는 것은 견고한 유닛 테스트의 필수 요소입니다. 이 글에서는 Jest에서 예외 타입과 메시지를 검증하는 방법을 5단계로 정리합니다. 특히 AVA(Test Anything)의 t.throws 문법에 익숙한 개발자가 Jest로 마이그레이션할 때 자주 헷갈리는 부분을 중심으로 설명합니다.
1. 문제 정의
기존에 AVA를 사용하던 환경에서는 t.throws의 두 번째 인자로 예외 타입을 넘겨 검증할 수 있었습니다.
| |
이 코드를 Jest로 다시 작성하려고 할 때, t.throws처럼 예외 타입을 직접 지정하는 문법이 눈에 띄지 않아 어떻게 해야 할지 막막한 상황입니다.
핵심 질문: “Jest에서 던져진 예외가
TypeError,ReferenceError등 어떤 타입인지 테스트할 수 있는가?”
2. 원인 탐구
Jest에는 AVA의 t.throws와 1:1로 대응하는 “예외 타입을 두 번째 인자로 받는” 메서드가 없습니다. 대신 Jest는 매처(matcher) 방식, 즉 expect()에 함수를 전달하고 .toThrow() 매처를 호출하는 방식으로 동일한 검증을 수행합니다.
이 구조적 차이가 “Jest에서 예외 타입을 어떻게 테스트하지?“라는 혼란의 근본입니다. AVA는 함수를 실행하고 그 결과(던져진 예외)를 반환하는 반면, Jest는 expect에 실행하지 않은 함수 참조를 넘겨야 한다는 점이 핵심입니다.
3. 근본 원인 분석
혼란의 진짜 원인은 다음 두 가지입니다.
① expect는 함수 자체를 받아야 한다
.toThrow()는 expect에 전달된 값이 (a) 실행됐을 때 예외를 던지는 함수이거나, (b) new Error에 넣을 수 있는 메시지 문자열, (c) 예외 생성자(Error subclass)여야 합니다. 만약 expect(x())처럼 함수를 호출한 결과를 넘기면, JavaScript는 expect가 실행되기 전에 그 줄에서 x()를 먼저 호출해 버려 예외가 테스트 코드 밖에서 즉시 발생하고 테스트가 실패합니다.
| |
② AVA와 Jest의 문법적 관용구가 다르다
AVA는 t.throws(fn, TypeError)로 “타입을 두 번째 인자"로 받지만, Jest는 expect(fn).toThrow(TypeError)로 “타입을 매처의 인자"로 받습니다. 같은 목표를 향한 표현 방식의 차이일 뿐, 둘 다 예외 타입 검증이 가능합니다.
| 구분 | AVA | Jest |
|---|---|---|
| 예외 타입 검증 | t.throws(fn, TypeError) | expect(fn).toThrow(TypeError) |
| 메시지 검증 | t.is(error.message, '...') | expect(fn).toThrow('...') |
| 전달 대상 | 함수를 실행해 예외를 반환 | 함수 참조를 expect에 전달 |
4. 코드 해결책
4-1. 예외 타입만 검증하기
expect(function).toThrow(예외 생성자) 형태로 함수를 전달합니다.
| |
toThrow의 인자로 TypeError, ReferenceError 등 Error의 하위 클래스 생성자를 넣으면 해당 타입과 일치할 때만 통과합니다.
4-2. 예외 타입과 메시지 함께 검증하기
타입과 메시지를 각각 toThrow로 검증합니다. 같은 함수를 여러 번 호출해도 내부에서 예외를 던지기만 하면 되므로 안전합니다.
| |
toThrow("UNKNOWN ERROR")는 에러 메시지에 해당 문자열이 포함되어 있으면 통과합니다. 정확히 일치하는 메시지를 검증하고 싶다면 toThrow 대신 toThrowError 또는 정규식(/UNKNOWN ERROR/)을 활용할 수 있습니다.
4-3. 기존 함수(인자 포함) 테스트하기
이미 존재하는 함수가 특정 인자로 호출될 때 예외를 던지는지 확인하려면, 그 함수를 익명 함수로 감싸서 expect에 넘겨야 합니다.
| |
익명 함수 래퍼가 필요한 이유는 바로 위 “근본 원인 분석"에서 설명했듯, expect가 실행될 함수 참조를 요구하기 때문입니다. expect(http.get(yourUrl, yourCallbackFn)).toThrow(...)처럼 쓰면 http.get이 먼저 실행되어버립니다.
4-4. (참고) 비동기 함수의 예외 테스트
비동기 함수가 거부(reject)하는 경우에는 toThrow 대신 .rejects.toThrow를 사용합니다.
| |
5. 향후 예방 조치
- 항상 함수 참조를 넘긴다 —
expect(비동기가 아닌 함수).toThrow(...)에서 괄호를 실수로 붙이지 않도록expect(() => fn())형태의 익명 함수 래퍼를 기본 습관으로 삼습니다. - 타입과 메시지를 각각 검증한다 — 타입만 확인하지 말고
toThrow(TypeError)와toThrow('메시지')를 함께 사용해 더 명확한 실패 원인을 얻습니다. - 검증 기준을 정한다 — 메시지가 자주 바뀐다면 전체 문자열보다 정규식(
/UNKNOWN ERROR/)이나 부분 문자열로 검증해 테스트의 취약성을 줄입니다. - 비동기 예외는 rejects를 사용한다 — async 함수는
await expect(...).rejects.toThrow(...)로 작성합니다. - eslint-plugin-jest 사용 시 유의 — try/catch 기반 방식은
no-conditional-expect규칙에 걸릴 수 있으므로, 기본적으로toThrow매처 방식을 권장합니다.
참고 출처: StackOverflow — How to test the type of a thrown exception in Jest (질문 46042613, 채택 답변 득표 845+)