JUnit5 테스트 코드 작성법

단위 테스트

단위 테스트는 애플리케이션의 각 기능이 독립적으로 올바르게 동작하는지를 검증하는 프로세스이다.

JUnit 5

JUnit 5는 단위 테스트 프레임워크로, XUnit 계열 중 하나이다.

  • JUnit Platform: JVM에서 동작하는 테스트 프레임워크로, TestEngine 인터페이스를 정의하고 테스트를 발견, 실행 및 결과를 보고하는 역할을 한다.
  • JUnit Jupiter: JUnit 5를 위한 테스트 API를 사용하는 TestEngine 구현체로, Jupiter API를 통해 작성한 테스트 코드를 실행한다.
  • JUnit Vintage: JUnit 3 및 4 버전으로 작성한 테스트 코드를 실행하는 TestEngine 구현체이다.

JUnit 5는 Java 8 이상과 Spring Boot 2.2 이상에서 사용 가능하며, 의존성을 추가하여 사용할 수 있다.

JUnit 5의 주요 어노테이션

1. 테스트 메서드와 생명 주기

  • @Test: 이 어노테이션은 메서드가 테스트 메서드임을 나타낸다. JUnit은 이 어노테이션이 붙은 메서드를 자동으로 테스트로 인식하여 실행한다.
  • @BeforeEach: 각 테스트 메서드 실행 전에 특정 작업을 수행해야 할 때 사용된다. 주로 목업 데이터를 설정하는 데 유용하다.
private Calculator calculator;
    
    @BeforeEach
    void setUp() {
        // 각 테스트 메서드 실행 전에 Calculator 객체를 초기화
        calculator = new Calculator();
    }
    
    @Test
    void testAddition() {
        assertEquals(5, calculator.add(2, 3));
    }
    
    @Test
    void testSubtraction() {
        assertEquals(1, calculator.subtract(3, 2));
    }
  • @AfterEach: 각 테스트 메서드 실행 후 특정 작업을 수행할 때 사용된다. 이 메서드는 모든 테스트 메서드 실행 후에 실행된다.
  • @BeforeAll: 모든 테스트 메서드 실행 전 한 번만 특정 작업을 수행한다.
  • @AfterAll: 모든 테스트 메서드 실행 후 한 번만 특정 작업을 수행하는 static 메서드이다. BeforeAll/AfterAll 메서드는 static으로 정의되어야 한다.
  • @Nested: 테스트 클래스 안에 내부 클래스를 정의하여 테스트를 계층화할 수 있다. 내부 클래스는 부모 클래스의 멤버 필드에 접근할 수 있는 장점이 있다.
@Nested
    class 우승자_출력 {
        @Test
        void 여러_우승자_출력_테스트() {
            // Given
            List<String> winners = List.of("pobi", "woni");

            // When
            Application.printWinner(winners);

            // Then
            assertThat(output()).contains("pobi, woni");
        }
    }

image

2. 유틸리티 어노테이션

  • @DisplayName: 메서드 이름에 사용할 수 없는 공백, 특수 문자, 이모지를 활용하여 사용자 친화적으로 표시할 수 있다. 이를 통해 테스트의 의도를 명확하게 전달할 수 있다.
@DisplayName("덧셈 테스트")
    @Test
    void testAddition() {
        assertEquals(2, 1 + 1);
    }

3. 실행 조건

  • @Disabled: 테스트를 실행하고 싶지 않은 클래스나 메서드에 붙인다. 이 어노테이션이 붙은 테스트는 JUnit에서 실행되지 않는다.
@Disabled
    @Test
    void skippedTest() {
        // 이 테스트는 실행되지 않는다.
    }

4. 테스트 종류

  • @ParameterizedTest: 여러 입력 값에 대해 반복적으로 테스트를 수행할 수 있게 해주는 어노테이션이다. 여러 파라미터 어노테이션과 함께 사용된다.

[[codingPractices] Parameterized Tests 사용법](https://velog.io/@grayson1999/codingPractices-Parameterized-Tests-%EC%82%AC%EC%9A%A9%EB%B2%95)

@ValueSource: 기본 값들로 반복 테스트를 수행할 수 있다.
@NullSource: null 값을 테스트하는 데 사용된다.
@NullAndEmptySource: null과 빈 값을 주입하여 테스트할 수 있다.
@EmptySource: 빈 객체(예: 빈 문자열)를 테스트한다.
@CsvSource: 입력 값과 예상 값을 전달하여 테스트할 수 있다.

@ParameterizedTest
    @CsvSource({"1, 2, 3", "2, 3, 5", "3, 5, 8"})
    void testAddition(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

  • @RepeatedTest: 지정된 횟수만큼 테스트를 실행할 수 있도록 해주는 어노테이션이다.
@RepeatedTest(5)
    void repeatedTest() {
        // 이 테스트는 5번 실행된다.
    }

AssertJ

Assertions는 테스트 케이스의 수행 결과를 판별하는 메서드로, 모든 JUnit Jupiter assertions는 static 메서드로 제공된다. Assertions를 통해 실제 결과와 예상 결과를 비교할 수 있다.

AssertJ 사용법

AssertJ의 기본 문법 구조

assertThat(검증하려는 대상).검증메서드(원하는 결과);

이 구조를 통해 간단하고 직관적인 방식으로 테스트를 작성할 수 있다. 검증하려는 대상을 지정한 후, 그에 대한 검증 메서드를 호출하여 원하는 결과와 비교할 수 있다.

주요 검증 메서드

아래는 분류별로 정렬한 표입니다:

분류 검증 메서드 설명
boolean isTrue() 조건이 true인지 검증한다.
boolean isFalse() 조건이 false인지 검증한다.
object isEqualTo(expected) 기대하는 값과 동일한지 비교한다.
object isNotEqualTo(unexpected) 예상하지 않는 값과 다른지 확인한다.
object isSameAs(expected) 두 객체가 동일한 인스턴스인지 비교한다.
object isNotSameAs(unexpected) 두 객체가 서로 다른 인스턴스인지 확인한다.
object isNull() 주어진 값이 null인지 확인한다.
object isNotNull() 주어진 값이 null이 아님을 검증한다.
iterable/array contains(expected) 주어진 값이 컬렉션이나 문자열에 포함되는지 확인한다.
iterable/array doesNotContain(unexpected) 주어진 값이 컬렉션이나 문자열에 포함되지 않는지 확인한다.
iterable/array hasSize(size) 컬렉션이나 배열의 크기가 주어진 값과 동일한지 검증한다.
iterable/array isEmpty() 컬렉션이나 문자열이 비어 있는지 확인한다.
iterable/array containsExactly(elements) 컬렉션이나 배열이 정확히 주어진 요소들만 포함하는지 확인한다.
iterable/array doesNotContainAnyElementsOf(expected) 주어진 컬렉션에 예상하지 않는 요소가 포함되어 있지 않은지 확인한다.
string startsWith(prefix) 문자열이 주어진 접두사로 시작하는지 확인한다.
string endsWith(suffix) 문자열이 주어진 접미사로 끝나는지 확인한다.
throwable hasMessage(message) 예외 메시지가 특정 문자열과 일치하는지 확인한다.
throwable hasNoCause() 예외가 원인이 없는지 검증한다.

이 표는 분류별로 정렬하여 각 검증 메서드와 그 설명을 쉽게 확인할 수 있도록 구성되었습니다.

예외 검증

Java 8에서 assertThatThrownBy(ThrowingCallable)를 사용하여 Throwable을 캡처한 후 assert한다.

import static org.assertj.core.api.Assertions.*;

assertThatThrownBy(() -> {
    // 예외 발생 코드
}).isInstanceOf(IndexOutOfBoundsException.class)
  .hasMessageContaining("Index: 2, Size: 2");

assertThatExceptionOfType(ExceptionType).isThrownBy() 구문을 사용해 작성할 수 있다.

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

assertThatExceptionOfType(IndexOutOfBoundsException.class)
    .isThrownBy(() -> {
        // 예외 발생 코드
    }).withMessageMatching("Index: \\d+, Size: \\d+");
  • 자주 발생하는 Exception의 경우 Exception별 메서드를 제공하고 있다.
    • assertThatIllegalArgumentException()
    • assertThatIllegalStateException()
    • assertThatIOException()
    • assertThatNullPointerException()

BDD 애호가를 위한 when, then 단계 분리 방법

@Test
public void testException() {
    // given some preconditions

    // when
    Throwable thrown = catchThrowable(() -> { throw new Exception("boom!"); });

    // then
    assertThat(thrown).isInstanceOf(Exception.class)
                      .hasMessageContaining("boom");
}

JUnit과 AssertJ의 조합

JUnit은 테스트의 실행 및 구조에 중점을 두고 있으며, 테스트 메서드의 설정, 실행 및 생명 주기를 관리하는 기능을 제공한다. 하지만 표현력은 상대적으로 부족하다.

반면, AssertJ는 자연어에 가까운 방식으로 작성할 수 있도록 하여 테스트 코드의 가독성을 높이고, 결과를 직관적으로 이해할 수 있도록 돕는다.

AssertJ의 메서드는 일반적인 조건문 형태로 작성되기 때문에 누구나 쉽게 읽고 이해할 수 있다.

위로 스크롤