[Java] JUnit5

우테코 2주차 미션에서 단위 테스트를 짜야하는 요구조건이 나왔다.

우선 개괄적인 이해를 위해 테코톡 영상을 시청하며 정리해본다.

[10분 테코톡] 🌊 바다의 JUnit5 사용법

필수 참고 사이트


1. JUnit?

  • 단위 테스트 프레임워크

JUnit5

  • JUnit Platform 위에 Jupiter API를 통해 구동
    • Platform : 테스트를 실행시켜주는 런처 제공. TestEngine API 제공
    • Jupiter : JUnit5를 지원하는 TestEngine API 구현체

2. Annotations

2-1. @Test annot.

  • 테스트 메서드라는 것을 나타내는 어노테이션
  • JUnit4와 다르게 어떠한 속성도 선언하지 않는다.
// JUnit4
@Test(expected = Exception.class)
void create() throws Exception {
    ...
}

// JUnit5
@Test
void create() {
    ...
}

2-2. 생명주기(LifeCycle) annot.

  • @BeforeAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 딱 한 번 실행되는 메서드
  • @AfterAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 후에 딱 한 번 실행되는 메서드


  • @BeforeEach: 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 실행되는 메서드
  • @AfterEach : 해당 클래스에 위치한 모든 테스트 메서드 실행 후에 실행되는 메서드
    • 매 테스트 메서드마다 새로운 클래스를 생성(new)하여 실행 -> 비효율적!

BeforeAll vs. BeforeEach

@BeforeAll

@Test
@Test
@Test

------

@BeforeEach
@Test

@BeforeEach
@Test

@BeforeEach
@Test
  • 단위 테스트들이 @BeforeAll 선언 이후 조건들에 대해 어떠한 변경도 하지 않는다!
    • BeforeAll
  • 단위 테스트가 조건에 영향을 미친다!
    • BeforeEach
    • 테스트 매 실행시마다 초기화

2-3. @Disabled annot.

  • 테스트를 하고 싶지 않은 클래서나 메서드에 붙이는 어노테이션
class Example {
    @Test
    @Disabled("문제가 해결될 때까지 테스트 중단")
    void test() {
        System.out.println("테스트1")
    }
    @Test
    void test() {
        System.out.println("테스트2")
    }
}

2-4. @DisplayName annot.

  • 어떤 테스트인지 쉽게 표현할 수 있도록 해줌
  • 공백, Emoji, 특수문자 등을 모두 지원
@DisplayName("테스트 이름 정해")
class Example {
    @Test
    @DisplayName("우아한 테스트")
    void test() {
    }
}

2-5. @RepeatedTest annot.

  • 특정 테스트를 반복시키고 싶을 때 사용
    • ex. 성능적인 이슈를 확인할 때
  • 반복 횟수와 반복 테스트 이름 설정 가능
@RepeatedTest(10)
@DisplayName("반복 테스트1")
void repeatedTest() {

}

@RepeatedTest(value = 10, name = '{displayName}  {currentRepetition} of {totalReptitions}')
@DisplayName("반복 테스트2")
void repeatedTest() {

}

2-6. @ParameterizedTest annot.

  • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행할 때 사용
    • 반복문 사용보다 가독성 향상
    • 테스트 성격, 목적 명료화 가능
@ParameterizedTest
@CsvSource(value = {"ACE,ACE:12", "ACE,ACE,ACE:13", "ACE,ACE,TEN:12"}, delimeter = ':')
@DisplayName("에이스 카드가 여러 개일 때 합 구하기")
void calculateCardSumWhenAceIsTwo(final String input, final int expected) {
    for (final String number : inputs) {
        final CardNumber cardNumber = CardNumber.valueOf(number)
        dealer.receiveOneCard(new Card(cardNumber, CardType.CLOVER));
    }
    assertThat(dealer.calculateScore()).isEqaulTo(expected);
}

2-7. @Nested annot.

  • 테스트 클래스 안에서 내부 클래스를 정의해 테스트를 계층화 할 때 사용
  • 내부클래스는 부모클래스의 멤버 필드에 접근 가능
  • Before / After와 같은 생명주기 테스트에 관계된 메소드들도 계층에 맞춰 동작
public class DisplayNameTest {

    @Test
    public void testAsuccess() { /* */ }

    @Test
    public void testAfail() { /* */ }

    @Test
    public void test1success() { /* */ }

    @Test
    public void test1success() { /* */ }

    @Test
    public void test2success() { /* */ }

    @Test
    public void test2fail() { /* */}
}

아래와 같이 UnitTest 리팩터링 가능

public class DisplayNameTest {

    @Nested
    class testA {

        @Test
        public void success() { /* */ }

        @Test
        public void fail() { /* */ }
    }

    @Nested
    class testNumber {

        @Nested
        class test1 {

            @Test
            public void success() { /* */ }

            @Test
            public void fail() { /* */ }
        }

        @Nested
        class test2 {

            @Test
            public void success() { /* */ }

            @Test
            public void fail() { /* */ }

        }
    }
}

image


3. Assertions

  • 사전적 의미 : 주장, 행사, 단정문
  • 테스트 케이스의 수행 결과를 판별하는 메서드
  • 모든 JUnit Jupiter Assertions는 statc 메서드

(단, 아래는 JUnit5에서 추가된 몇가지 사항)

3-1. assertAll(executables…)

  • 매개변수로 받는 모든 테스트코드를 한 번에 실행
  • 오류가 나도 끝까지 실행한 뒤 한 번에 모아서 출력
List<String> owners = Arrays.asList("Betty Davis", "Eduardo Rodriquez");

// assert
assertAll(
    () -> assertTrue(owners.contains("Betty Doe"), "Contains Betty Doe"),
    () -> assertTrue(owners.contains("John Doe"), "Contains John Doe"),
    () -> assertTrue(owners.contains("Eduardo Rodriquez"), "Eduardo Rodriquez")
);

3-2. assertThrows(expectedType, executable)

  • 예외 발생을 확인하는 테스트
  • executable의 로직이 실행되는 도중 expectedType의 에러를 발생시키는지 확인
//JUnit4
//예외가 발생하는지 여부만 검증 가능
@Test(expected = Exception.class)
void create() throws Exception {
    ...
}

// JUnit5
// 예외 발생 검증 + 예외 상태 검증
@Test
void exceptionThrow {
    Exception e = assertThrows(Exception.class,() -> new Test(-10));
    assertDoesNotThrow(() -> System.out.println("Do Something"));
}

3-3. assertTimeout(duration, executable)

  • 특정 시간 안에 실행이 완료되는지 확인
  • Duration : 원하는 시간
  • Executable : 테스트 할 로직
    @Test
    @DisplayName("타임아웃 준수")
    void assert_timeout1() {
        assertTimeout(Duration.ofMillis(100), () -> {
            new Study(10, 10);
            Thread.sleep(300);
        });
    void assert_timeout2() {
        assertTimeout(Duration.ofMinutes(2), () -> {
            new Study(10, 10);
            Thread.sleep(10);
        });
    }

4. Assumption

  • 전제문이 ture라면 실행, false라면 종료
  • assumeTure : false일 때 이후 테스트 전체가 실행되지 않음
  • assumingThat : 파라미터로 전달된 코드블럭만 실행되지 않음
    • CI 등 특정 환경에서만 테스트를 진행해야 하는 경우 사용 가능
    void dev_env_only() {
        assumeTrue("DEV".equals(System.getenv("ENV")), () -> "개발 환경이 아닙니다.");
        assertEqauls("A", "A"); //위에가 false라면, 단정문이 실행되지 않음
    void some_test() {
        assumingThat("DEV".equals(System.getenv("ENV")),
        () -> {
            assertEquals("A", "B"); //단정문이 실행되지 않음
        });
        assertEqauls("A", "A"); //단정문이 실행됨
    }