pytest에서 예외 발생을 올바르게 검증하는 방법 (pytest.raises)
1. 문제 정의
테스트 코드에서 “이 함수가 특정 예외를 던져야 한다"는 조건을 검증하려고 할 때, 많은 개발자가 다음과 같이 try/except와 pytest.fail을 조합해서 작성하곤 합니다.
| |
이 테스트를 실행하면 결과는 다음과 같습니다.
| |
문제는 명확합니다. 테스트가 실패(Fail)로 표시되긴 하지만, 정작 어떤 예외가 어디서 발생했는지에 대한 원본 traceback이 전혀 출력되지 않습니다. Failed: integer division or modulo by zero 라는 메시지만 나올 뿐, ZeroDivisionError가 발생한 실제 위치나 호출 스택은 확인할 수 없습니다.
2. 원인 탐구
왜 이런 문제가 발생할까요? 그 이유는 try/except 블록이 예외를 이미 잡아버렸기(caught) 때문입니다.
| |
whatever()에서 ZeroDivisionError가 발생하면, 이 예외는 except ZeroDivisionError 절에서 잡혀 변수 exc에 담깁니다. 이 시점에서 원본 예외의 traceback 정보는 손실된 상태입니다.
그런 다음 pytest.fail(exc, pytrace=True)를 호출하면, pytest.fail은 전달된 메시지(exc의 문자열 표현)만 사용해 테스트를 실패 처리합니다. 결과적으로:
- 원본 예외 타입 정보는 사라지고
- 발생 위치(traceback) 정보는 사라지고
- 단순한 실패 메시지만 남습니다
게다가 이 방식은 골치아픈 부작용도 있습니다. 만약 whatever()가 예외를 던지지 않는다면, except 블록은 실행되지 않고 pytest.fail도 호출되지 않아 테스트가 그냥 통과해버립니다. 즉, 예외가 발생하지 않는 잘못된 상황을 놓칠 수 있는 것입니다.
3. 근본 원인 분석
근본 원인은 잘못된 검증 도구의 선택에 있습니다.
try/except는 예외를 “처리(handle)“하는 용도이지 “검증(assert)“하는 용도가 아닙니다. 예외를 잡아서 처리하는 순간, 원본 예외 정보를 직접 관리해야 하며 traceback이 오염됩니다.pytest.fail은 단순히 테스트를 실패시키는 함수로, 예외 검증용으로 설계되지 않았습니다. 전달한 메시지를 그대로 출력할 뿐, 발생한 예외의 타입·메시지·traceback을 구조적으로 보여주지 못합니다.따라서
try/except + pytest.fail조합은 예외 검증이라는 목적에 맞지 않습니다.
pytest에는 이 목적을 위해 설계된 전용 도구가 있습니다. 바로 pytest.raises 컨텍스트 매니저입니다. pytest.raises는 블록 안에서 지정한 예외가 발생하는지 확인하고, 발생했을 때 예외 객체를 반환해 추가 검증이 가능하게 해줍니다. 예외가 발생하지 않으면 테스트를 실패시킵니다.
4. 코드 해결책
pytest.raises(Exception) 컨텍스트 매니저를 사용하면 됩니다. 다음은 공식 문서에서 권장하는 올바른 패턴입니다.
4-1. 기본 사용법 — 예외가 발생하는지 확인
| |
with pytest.raises(Exception): 블록 안의 코드가 Exception을 던지면 테스트는 통과합니다. 예외를 발생시키지 않으면 테스트는 실패합니다.
4-2. 예외 객체 정보 확인
as e_info를 사용하면 발생한 예외 객체를 받아와서 메시지나 타입을 추가 검증할 수 있습니다.
| |
e_info.value에 실제 예외 객체가 들어 있어, 예외 메시지나 속성을 검증할 수 있습니다.
4-3. 발생하지 않으면 실패하는 경우
| |
x = 1 / 1은 예외를 발생시키지 않으므로 pytest.raises는 이 테스트를 실패시킵니다. 이처럼 “예외가 반드시 발생해야 하는데 발생하지 않은” 경우를 정확히 잡아냅니다.
4-4. 하지 말아야 할 패턴 (Bad style)
solution에서 지적하는 잘못된 예시입니다. try/except와 assert를 조합한 다음 패턴은 작동하더라도 “좋지 않은 스타일"이며 테스트 결과 정보가 부실해집니다.
| |
이런 패턴은 테스트가 그냥 “pass” 또는 “fail"로만 표시되고, pytest.raises를 사용했을 때보다 훨씬 적은 정보를 제공합니다.
4-5. 방식 비교 표
| 방식 | 예외 검증 가능 | 원본 traceback 출력 | 예외 객체 접근 | 예외 미발생 시 동작 |
|---|---|---|---|---|
try/except + pytest.fail | 부분적 | ❌ 손실 | 부분적 | ❌ 통과해버림 (위험) |
try/except + assert | 부분적 | ❌ | ❌ | 상황에 따라 오동작 |
pytest.raises(Exception) | ✅ | ✅ | ✅ (e_info.value) | ✅ 실패 처리 |
5. 향후 예방 조치
앞으로 같은 실수를 피하려면 다음 규칙을 지키세요.
- 예외 검증은 반드시
pytest.raises를 사용하세요.try/except + pytest.fail이나try/except + assert조합은 피하세요. - 예외 메시지까지 검증해야 한다면
as e_info를 사용해e_info.value로 접근하세요. 예외 타입만 확인할 때는 생략해도 됩니다. - 정확한 예외 타입을 지정하세요.
pytest.raises(Exception)보다pytest.raises(ZeroDivisionError)처럼 구체적인 타입을 지정하는 것이 더 엄격하고 명확한 테스트가 됩니다. - 예외가 “발생하지 않는 경우"도 함께 검증하세요.
pytest.raises는 예외가 발생하지 않으면 테스트를 실패시키므로, 이 특성을 활용해 예외 경로를 놓치지 않도록 하세요. - 코드 리뷰 시
try/except로 예외 검증을 흉내 낸 테스트 코드가 보이면pytest.raises로 교체하도록 안내하세요.
이 규칙을 따르면 테스트가 실패했을 때 원본 traceback과 예외 정보가 그대로 출력되어 디버깅 시간을 크게 줄일 수 있습니다.
출처: StackOverflow 23337471 - How do I properly assert that an exception gets raised in pytest?