4 분 소요

이번 스터디 주제로 JUnit5에 대해 공부한 것을 정리해봤습니다.

JUnit5

자바 개발자가 가장 많이 사용하는 테스트 프레임워크입니다.

자바 8 버전 이상이 필요하며 크게 3개의 모듈로 구성되어 있습니다.

  • JUnit Platform: 테스트를 실행해주는 런처와 TestEngine API를 제공
  • JUnit Jupiter: JUnit5를 테스트할 수 있는 TestEngine API의 구현체
  • JUnit Vintage: JUnit 3, 4를 지원하는 TestEngine API의 구현체

설치

스프링 부트는 JUnit을 기본 테스트 프레임워크로 채택하고 있기 때문에 프로젝트 생성 시 따로 설치를 할 필요가 없지만 스프링 레거시를 사용한다면 하단의 라이브러리 설치가 필요합니다.

메이븐

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.9.1</version>
    <scope>test</scope>
</dependency>

그래들

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'

기본 어노테이션

  • @Test : 테스트 메서드임을 명시
  • @BeforeEach : 각각의 테스트 메서드가 실행되기 전에 실행되어야 하는 메서드를 명시
  • @AfterEach : 각각의 테스트 메서드가 실행된 후 실행
  • @BeforeAll : 테스트가 실행되기 전 단 한 번만 실행
  • @AfterAll : 테스트가 완전히 끝난 후 단 한 번만 실행
  • @Tag : 테스트를 필터링하기 위해 사용
  • @Timeout : 테스트 메서드의 제한시간을 설정하며 시간 안에 테스트가 끝나지 않으면 실패
  • @Disabled : 테스트를 비활성화
  • @DisplayName : 테스트의 이름을 붙여줄 때 사용

Assertions

assertions는 테스트 코드를 작성하며 기대하는 값 또는 상황과 일치하는 지 검증하기 위해 사용합니다. JUnit Jupiter는 기본 assertions를 내장하고 있고 org.junit.jupiter.api.Assertions 라이브러리로 제공합니다.

@Test
@DisplayName("스터디를 생성 시 인스턴스가 존재해야 한다.")
void create_study_is_not_null() {
    Study study = new Study(10);

    assertNotNull(study);
}

@Test
@DisplayName("스터디를 생성 시 DRAFT 상태여야 한다.")
void create_study_status_should_draft() {
    Study study = new Study(10);

    assertEquals(
            StudyStatus.DRAFT,
            study.getStatus(),
            () -> "스터디를 처음 만들면 " + StudyStatus.DRAFT + " 상태이다.");
}

@Test
@DisplayName("스터디를 생성 시 DRAFT 상태여야 하며 인원수가 0보다 커야한다.")
void create_study_limit_greater_than_zero() {
    Study study = new Study(10);

    assertAll(
            () -> assertEquals(StudyStatus.DRAFT, study.getStatus()),
            () -> assertTrue(study.getLimit() > 0, "스터디 최대 참석 인원은 10명이다.")
    );
}

@Test
@DisplayName("스터디 인원을 0보다 작게 설정 시 예외가 발생한다.")
void throw_exception_limit_less_than_zero() {
    IllegalArgumentException exception =
            assertThrows(IllegalArgumentException.class, () -> new Study(-10));

    assertEquals("limit은 0보다 커야 한다.", exception.getMessage());
}

@Test
@DisplayName("스터디 인스턴스는 20ms 안에 생성되어야 한다.")
void create_study_duration_10ms() {
    assertTimeout(Duration.ofMillis(20), () -> {
            new Study(10);
            Thread.sleep(50);
    });
}

각 assertions 정적 메서드들은 String 또는 Supplier<String> 타입의 람다 인스턴스를 전달하여 메시지를 출력하도록 할 수 있습니다.

Assumptions

특정한 조건 또는 환경에서의 테스트를 작성하기 위한 방법으로 org.junit.jupiter.api.Assumptions 로 제공됩니다.

public class AssumptionTest {

    @Test
    void ci_env_test() {
        assumeTrue("CI".equals(System.getenv("ENV")));
    }

    @Test
    void dev_env_test() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
                () -> "Aborting test: not on developer workstation");
    }

    @Test
    void all_env_test() {
        assumingThat("CI".equals(System.getenv("ENV")),
                () -> assertEquals(2, new Study(2).getLimit()));
        
        assertEquals(10, new Study(10).getLimit());
    }

		@Test
    @EnabledOnJre(value = {JRE.JAVA_8, JRE.JAVA_11})
    void test_on_java8_or_java11() {
        assertNotNull(new Study(5));
    }
}

@Enable__ , @Disable__ 형태의 어노테이션이 제공되며 개발환경과 로컬환경, 환경변수, JAVA 버전 또는 OS 등의 조건을 명시하여 테스트를 작성할 수 있습니다.

이 때 명시된 조건에 부합하지 않으면 @Disable 처리되어 해당 테스트 메서드가 실행되지 않습니다.

태그와 필터링

public class TagTest {

    @Test
    @Tag("fast")
    void fast_test() {
        assertEquals("HELLO", new String("HELLO"));
    }

    @Test
    void just_test() {
        assertEquals("WORLD", new String("WORLD"));
    }
}

테스트 클래스와 메서드는 @Tag 어노테이션을 통해 태그할 수 있습니다. 태그를 정의한 후 테스트 환경을 설정하여 특정 태그들만 필터링해서 테스트를 수행할 수 있도록 해줍니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("fast")
public @interface Fast {
}

또 위와 같이 직접 작성한 커스텀 어노테이션을 사용할 수도 있습니다.

반복 테스트

@Slf4j
public class RepeatTest {

    private static int count = 0;

    @RepeatedTest(value = 10)
    void repeatTest_1() {
        log.info("count : {}", count++);
    }

    @DisplayName("반복 테스트")
    @RepeatedTest(value = 10, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
    void repeatTest_2() {
        log.info("count : {}", count++);
    }
}

@RepeatedTest 어노테이션을 붙이면 해당 메서드를 설정한 값만큼 반복시킬 수 있습니다.

repetition n of value 의 형태로 테스트의 이름이 출력되며 name 설정으로 이를 변경할 수 있습니다.

@ParameterizedTest
@ValueSource(strings = {"Hello", "World", "Test", "JUnit5"})
void repeatTest_3(String message) {
    log.info("message : {}", message);
}

@ParameterizedTest 어노테이션은 테스트 메서드에 인자를 넘겨서 수행할 수 있는 기능을 제공합니다.

테스트에 넘길 인자는 @ValueSource 로 정의할 수 있으며 기본 데이터 타입부터 객체까지 전달할 수 있습니다.

테스트 클래스의 라이프 사이클

반복 테스트 예제에서 count 변수를 static 으로 선언하지 않으면 어떻게 될까요?

0으로만 찍히는 것을 보면 테스트 메서드를 반복 수행할 때마다 새로운 테스트 인스턴스를 생성하는 것을 알 수 있습니다.

JUnit의 기본 전략은 테스트 메서드마다 테스트 인스턴스를 새로 생성하도록 설정되어 있습니다. 테스트 메서드를 독립적으로 실행해서 사이드 이펙트를 방지하기 위함이며 변경 가능합니다.

@TestInstance(Lifecycle.PER_CLASS) 어노테이션을 사용하면 같은 인스턴스 안에서 모든 테스트 메서드를 실행할 수 있습니다. 이 경우에는 인스턴스 변수를 모든 테스트 메서드가 공유하므로 @BeforeEach 또는 @AfterEach 로 상태를 초기화해줄 필요가 있습니다.

테스트 실행 순서

기본적으로 JUnit은 테스트 메서드 실행 순서를 보장하지 않습니다. 테스트는 언제든 독립적으로 실행되어 성공해야하기 때문인데 순서를 직접 정의할 수도 있습니다.

@TestMethodOrder 어노테이션으로 MethodOrderer를 구현해서 사용하면 순서를 지정할 수 있습니다.

  • DisplayName : @DisplayName 기반 정렬
  • MethodName : 메서드 이름 정렬
  • OrderAnnotation : @Order 순서 기반 정렬
  • Random

의 구현체가 제공되며 커스텀해서 사용하는 방법도 존재합니다.

@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderTest {

    int count = 0;

    @Test
    @Order(1)
    void first_test() {
        log.info("count : {}", count);
    }

    @Test
    @Order(0)
    void second_test() {
        log.info("count : {}", count);
    }

    @Test
    @Order(1)
    void third_test() {
        log.info("count : {}", count);
    }

    @Test
    @Order(-5)
    void fourth_test() {
        log.info("count : {}", count);
    }
}

@Order 어노테이션에 순서를 명시하면 오름차순으로 테스트 메서드를 실행하게 됩니다.

이 때 정의한 순서가 같다면 JUnit 내부의 순서에 의해 실행됩니다.

참고자료 더 자바, 애플리케이션을 테스트하는 다양한 방법

댓글남기기