JUnit에서 예외 발생 검증 — @Test(expected) vs assertThrows()

1. 문제 정의

단위 테스트에서 “이 메서드는 특정 상황에서 특정 예외를 던져야 한다"를 검증할 때가 많습니다. 이 질문은 StackOverflow에서 조회 210만 회 이상 누적된 고전적인 주제입니다.

가장 흔하지만 장황한 구현이 try/catch + assertTrue 방식입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void testFooThrowsIndexOutOfBoundsException() {
    boolean thrown = false;
    try {
        foo.doStuff();
    } catch (IndexOutOfBoundsException e) {
        thrown = true;
    }
    assertTrue(thrown);
}

2. 원인 탐구

검증 의도는 같지만 표현하는 API가 JUnit 버전에 따라 달라졌습니다. 아래 표는 버전별 예외 검증 방법입니다.

JUnit 버전예외 검증 방법특징
JUnit 4.12 이하@Test(expected = X.class)예외 타입만 검증, 메시지 확인 불가
JUnit 4.13+org.junit.Assert.assertThrows예외 객체 반환, 메시지 검증 가능
JUnit 5org.junit.jupiter.api.Assertions.assertThrows람다 기반, assertThrowsExactly 지원
외부 라이브러리AssertJ / Google Truth메시지·원인 체이닝 검증 가능

3. 근본 원인 분석

try/catch + assertTrue 방식이 실무에서 문제가 되는 이유와, JUnit 버전 혼용 오해를 정리합니다.

증상 fingerprint

항목내용
증상테스트가 실패하지 않는데 검증이 빈번히 어긋나거나, assertThrows 미존재로 컴파일 에러 발생
발생 단계단위 테스트 실행 시 Maven mvn test / Gradle ./gradlew test
관련 도구Java, JUnit 4, JUnit 5, AssertJ, Google Truth
흔한 오해JUnit 4.12 프로젝트에 assertThrows를 무심코 사용하거나, JUnit 5에서 @Test(expected=…)를 사용
빠른 판단 기준먼저 사용 중인 JUnit 버전을 확인한 뒤 그 버전에 맞는 API 선택

원인 후보 매트릭스

원인 후보확인 방법맞을 때 증상해결 방향
JUnit 4.12 이하junit 의존성 버전 확인assertThrows 없다는 컴파일 에러@Test(expected=…) 또는 4.13+ 업그레이드
JUnit 4.13+pom/build.gradle 확인assertThrows 정상 동작org.junit.Assert.assertThrows
JUnit 5junit-jupiter 확인Assertions.assertThrows 존재org.junit.jupiter.api.Assertions
try/catch 방식코드 리뷰예외가 안 나면 실패 원인 모호assertThrows로 교체

잘못된 해결책 — try/catch + assertTrue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void testFooThrowsIndexOutOfBoundsException() {
    boolean thrown = false;
    try {
        foo.doStuff();
    } catch (IndexOutOfBoundsException e) {
        thrown = true;
    }
    assertTrue(thrown);
}

문제점:

  • 예외가 발생하지 않으면 assertTrue(thrown)이 실패하지만, 다른 타입의 예외가 나면 그대로 전파되어 검증 의도가 모호해집니다.
  • thrown 같은 플래그 변수로 코드가 장황해집니다.
  • 예외 메시지 검증이 어렵습니다.
  • assertThrows가 이 문제들을 API로 해결하므로 assertThrows를 우선합니다.

4. 코드 해결책

버전에 맞는 API를 사용해 예외 타입과 메시지를 검증합니다.

JUnit 4.12 이하 — @Test(expected = …)

1
2
3
4
5
@Test(expected = IndexOutOfBoundsException.class)
public void testIndexOutOfBoundsException() {
    ArrayList<String> emptyList = new ArrayList<>();
    emptyList.get(0); // 여기서 예외 발생
}

예외 타입만 확인하며 메시지는 검증할 수 없습니다.

JUnit 4.13+ / JUnit 5 — assertThrows

1
2
3
4
5
6
7
8
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;

IndexOutOfBoundsException ex = assertThrows(
    IndexOutOfBoundsException.class,
    () -> new ArrayList<>().get(0)
);
assertEquals("Index 0 out of bounds for length 0", ex.getMessage());
  • JUnit 4.13+는 import static org.junit.Assert.assertThrows;로 동일한 형태를 씁니다.
  • JUnit 5 하위 타입이 아닌 정확한 타입만 통과시키려면 assertThrowsExactly(ExpectedType.class, () -> ...)를 사용합니다.

검증 명령

1
2
3
4
5
# Maven
mvn test

# Gradle
./gradlew test
  • 예외 유발 테스트가 기대대로 통과하는지 확인합니다.

5. 향후 예방 조치

프로젝트의 JUnit 버전을 먼저 확인하고 그 버전에 맞는 API를 사용합니다.

확인 항목권장 예외 검증 API
JUnit 4.12 이하@Test(expected=…)
JUnit 4.13+org.junit.Assert.assertThrows
JUnit 5Assertions.assertThrows / assertThrowsExactly
AssertJ / Google TruthassertThatThrownBy 등 체이닝
  • 신규 테스트는 assertThrows 기반으로 작성합니다.
  • 기존 try/catch + assertTrue 방식은 점진적으로 assertThrows로 교체합니다.
  • 로컬과 CI에서 동일하게 검증 명령을 실행하여 버전 혼용으로 인한 재발을 막습니다.

DevTrace verdict

핵심 판단: 예외 검증이라는 의도는 같지만 JUnit 버전별로 API가 다르므로, 사용 중인 버전에 맞는 API를 선택하는 것이 핵심입니다.


출처: https://stackoverflow.com/questions/156503/how-do-you-assert-that-a-certain-exception-is-thrown-in-junit-tests