Exception을 테스트 할 때 거짓 음성을 주의하자

ds_chanin

·

2025. 2. 18. 01:33


거짓 음성

여기서 언급한 거짓 음성이 무엇인지 먼저 짚고 넘어가자~

거짓 음성이란 실제로는 실패해야 하는 테스트가 통과하는 경우를 의미한다.
다시말해 기능이 고장났는데 테스트가 통과하는 케이스를 의미한다.

 

보통 테스트의 정확도를 이야기할 때 거짓 음성과 거짓 양성에 대해 이야기를 한다.

대게 거짓 양성에 대해 더 중요하게 다루곤 하지만 코드리뷰를 하며 거짓 음성을 발생시키는 케이스를 많이 본 것 같다.

 

조금 더 정확히 짚고 넘어가면 기능이 실패한다기 보다 기능에 대해 테스트가 정확히 검증해주지 못하는 케이스라고 봐야할 것 같다.

 

예제 코드 1

그래서 Exception을 테스트하는 것과 어떤 관련이 있는가 싶을텐데

코드로 살펴보도록 하자~

public class MyCollection {
    private static final int FULL_SIZE = 3;
    private final List<Object> list;

    public MyCollection(List<Object> list) {
        this.list = list;
        this.validate();
    }

    private void validate() {
        if (list.size() >= FULL_SIZE) {
            throw new IllegalStateException("컬렉션에 아이템이 가득찼습니다. size: " + list.size());
        }
        if (list.isEmpty()) {
            throw new IllegalStateException("컬렉션에 아이템이 없습니다.");
        }
    }
}

 

위와 같은 일급컬렉션을 나타내는 클래스가 있고 이 일급컬렉션의 정책은 다음과 같다.

  1. 컬렉션의 아이템은 3개 이상이 될 수 없다.
  2. 컬렉션은 비어있을 수 없다.

이런 정책을 가진 컬렉션을 가지고 Exception에 대한 테스트를 잘못하는 경우 거짓 양성이 발생할 수 있다.

@DisplayName("일급컬렉션에 아이템이 없으면 exception이 발생한다.")
@Test
void test_10() {
    assertThatThrownBy(() -> new MyCollection(new ArrayList<>()))
        .isInstanceOf(IllegalStateException.class);
}

@DisplayName("일급컬렉션에 아이템이 가득차면 exception이 발생한다.")
@Test
void test_20() {
    assertThatThrownBy(() -> new MyCollection(new ArrayList<>(
        List.of()
    )))
        .isInstanceOf(IllegalStateException.class);
}

 

첫번째 테스트 코드는 문제가 없어보인다. 테스트에서 검증하고자 하는 빈 컬렉션을 전달하였고 그에 대한 exception을 잘 체크하였다.

두번째 테스트 코드는 테스트에서 검증하고자 하는 바와 다르게 실수로 아이템이 가득찬 컬렉션을 전달하지 않고 빈 컬렉션을 전달해버렸다. 

하지만 두번째 테스트 코드도 검증하고자 하는 exception의 종류가 같기때문에 테스트 코드는 성공하게 된다.

 

이처럼 테스트 코드의 타입만 검사해서는 테스트 하고자하는 대상에 대해 명확하게 테스트 했다고 보기 힘들다.

그러므로 다음과 같이 이러한 exception을 테스트할 때는 반드시 message와 같은 구체적인 사항도 함께 검사해주도록 하자. 그렇게 한다면 위와 같은 부분에서 발생한 실수를 잡아낼 수 있다.

 

@DisplayName("일급컬렉션에 아이템이 없으면 exception이 발생한다.")
@Test
void test_10() {
    assertThatThrownBy(() -> new MyCollection(new ArrayList<>()))
        .isInstanceOf(IllegalStateException.class)
        .hasMessage("컬렉션에 아이템이 없습니다.");
}

@DisplayName("일급컬렉션에 아이템이 가득차면 exception이 발생한다.")
@Test
void test_20() {
    assertThatThrownBy(() -> new MyCollection(new ArrayList<>(
        List.of(1, 2, 3)
    )))
        .isInstanceOf(IllegalStateException.class)
        .hasMessage("컬렉션에 아이템이 가득찼습니다. size: 3");
}

 

 

예제 코드 2

그런데 "에이 누가 저런 테스트 코드 실수를 합니까ㅋㅋ 난 아닌데용ㅋㅋ"

할 수 있다.

위와 같은 케이스에서만 문제가 발생하는게 아니다.

아래와 같은 상황에서도 명확하게 테스트하고자 하는 대상이 테스트 되지 않을 수 있다.

 

이번에는 어떠한 문장을 표현하는 값 객체가 있다고 하자.

public class LimitedCleanText {
    private static final int LIMIT_SIZE = 15;
    private final String text;

    public LimitedCleanText(String text) {
        this.text = text;
        this.validate();
    }

    private void validate() {
        if (text.contains("그지")) {
            throw new IllegalStateException("욕설이 포함되어 있습니다.");
        }
        if (text.length() > LIMIT_SIZE) {
            throw new IllegalStateException("텍스트 길이가 15을 넘었습니다.");
        }
    }
}
  1. '그지' 라는 욕설이 들어가선 안된다.
  2. 텍스트의 길이는 15자를 넘으면 안된다.

워터글래스툰 재밌습니다

 

그리고 아래와 같이 테스트 코드를 작성해보았다.

class LimitedCleanTextTest {

    @DisplayName("욕설이 포함된 텍스트를 생성하면 exception이 발생한다.")
    @Test
    void test_14() {
        String text = "그지같이 코드 짜지 말라고!!"; // 16자

        assertThatThrownBy(() -> new LimitedCleanText(text))
            .isInstanceOf(IllegalStateException.class);
    }

    @DisplayName("15자리 이상의 텍스트를 생성하면 exception이 발생한다.")
    @Test
    void test_23() {
        String text = "1234567890123456"; // 16자

        assertThatThrownBy(() -> new LimitedCleanText(text))
            .isInstanceOf(IllegalStateException.class);
    }

}

 

이번에는 아무런 문제가 없다.

정책에 따라 올바르게 검증되었다는 것도 확신할 수 있었고 분명했다.

 

그런데 다른 사람이 운영 코드에 수정을 가해서 검증 로직의 순서를 다음과 같이 바꾼다면?

  1. '그지' 라는 욕설이 들어가선 안된다. -> 텍스트의 길이는 15자를 넘으면 안된다.
  2. 텍스트의 길이는 15자를 넘으면 안된다. -> '그지' 라는 욕설이 들어가선 안된다.
private void validate() {
    if (text.length() > LIMIT_SIZE) {
        throw new IllegalStateException("텍스트 길이가 15을 넘었습니다.");
    }
    if (text.contains("그지")) {
        throw new IllegalStateException("욕설이 포함되어 있습니다.");
    }
}

 

 

기존 테스트 코드는 애석하게도 통과한다.

하지만 우리가 진정 검사하고자 했던 욕설에 대한 검증은 올바르게 되지 않는다.

왜냐하면 욕설을 검증하는 테스트 코드에 사용된 테스트 데이터가 '15자를 넘는 욕설을 포함한 문장'이기 때문이다.

@DisplayName("욕설이 포함된 텍스트를 생성하면 exception이 발생한다.")
@Test
void test_14() {
    String text = "그지같이 코드 짜지 말라고!!"; // 16자

    assertThatThrownBy(() -> new LimitedCleanText(text))
        .isInstanceOf(IllegalStateException.class);
}

 

따라서 이렇게 작성된 exception 테스트는 결코 안전한 테스트 코드라 할 수 없다.

 

반드시 exception에 대한 테스트를 수행한다면 exception을 특정할 수 있는 정보를 함께 테스트에 포함시켜 검증하도록 하자.