이전

JUnit 5

2.2+ 버전의 스프링 부트 프로젝트를 만든다면 기본으로 JUnit 5 의존성 추가 됨.

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

  • “단위 테스트를 작성하는 자바 개발자 93% JUnit을 사용함.”
  • 자바 8 이상을 필요로 함.
  • 대체제: TestNG, Spock, …

  • Platform: 테스트를 실행해주는 런처 제공. TestEngine API 제공.
  • Jupiter: TestEngine API 구현체로 JUnit 5를 제공.
  • Vintage: JUnit 4와 3을 지원하는 TestEngine 구현체.

테스트 이름 표시하기

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
  • Method와 Class 레퍼런스를 사용해서 테스트 이름을 표기하는 방법 설정.
  • 기본 구현체로 ReplaceUnderscores 제공
@DisplayName("스터디 만들기")
  • 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 애노테이션.
  • @DisplayNameGeneration 보다 우선 순위가 높다.

Assertion

org.junit.jupiter.api.Assertions.*

@Test
@DisplayName("스터디 만들기")
void create_new_study() {
    Study study = new Study(5);

    assertNotNull(study);
    // messages 를 람다식으로 작성하는 이유는?
    assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면" + StudyStatus.DRAFT + "여야 한다.");
    assertTrue(study.getLimit() > 0, () -> "스터디 최대 참석 인원은 0 보다 커야 한다.");
    IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Study(-10));
    assertEquals("limit 은 0보다 커야 한다.", exception.getMessage());

    assertTimeout(Duration.ofMillis(100), ()-> {
        new Study(10);
        Thread.sleep(50);
    });

    assertTimeoutPreemptively(Duration.ofMillis(100), ()-> {
        new Study(10);
        Thread.sleep(50);
    });

    assertAll(
            () -> assertNotNull(study),
            () -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면" + StudyStatus.DRAFT + "여야 한다."),
            () -> assertTrue(study.getLimit() > 0, () -> "스터디 최대 참석 인원은 0 보다 커야 한다."),
            () -> assertThrows(IllegalArgumentException.class, () -> new Study(-10))
    );

}
실제 값이 기대한 값과 같은지 확인 assertEqulas(expected, actual)
값이 null이 아닌지 확인 assertNotNull(actual)
다음 조건이 참(true)인지 확인 assertTrue(boolean)
모든 확인 구문 확인 assertAll(executables…)
예외 발생 확인 assertThrows(expectedType, executable)
특정 시간 안에 실행이 완료되는지 확인 assertTimeout(duration, executable)

마지막 매개변수로 Supplier 타입의 인스턴스를 람다 형태로 제공할 수 있다.

  • 복잡한 메시지 생성해야 하는 경우 사용하면 실패한 경우에만 해당 메시지를 만들게 할 수 있다.

조건에 따라 테스트 실행하기

@Test
@DisplayName("조건에 따라 테스트 실행하기")
void create_con_study() {
    String test_env = System.getenv("TEST_ENV");
    System.out.println("TEST_ENV:" + test_env);
    assumeTrue("LOCAL".equalsIgnoreCase(test_env));

    assumingThat("LOCAL".equalsIgnoreCase(test_env), ()->{
        System.out.println("local");
        Study actual = new Study(100);
        assertThat(actual.getLimit()).isGreaterThan(0);
    });
}


@Test
@DisplayName("조건에 따라 테스트 실행하기 annotation")
@EnabledOnOs({OS.MAC, OS.LINUX})
@EnabledOnJre(JRE.JAVA_8)
//    @DisabledOnOs(OS.MAC)
@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "LOCAL")
void create_con1_study() {
    String test_env = System.getenv("TEST_ENV");
    System.out.println("TEST_ENV:" + test_env);
    assumeTrue("LOCAL".equalsIgnoreCase(test_env));
}

태깅

@Test
@Tag("fast")
void test_tagging() {
    System.out.println("local 환경에서 테스트 fast");
}

@Test
@Tag("slow")
void test_tagging2() {
    System.out.println("ci 환경에서 테스트 slow");
}
  • 테스트 메소드에 태그를 추가할 수 있다.
  • 하나의 테스트 메소드에 여러 태그를 사용할 수 있다.
  • Intellij, Maven, Gradle 에서 사용하는 방법 구글 검색

커스텀태그

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("fast")
public @interface FastTest {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("slow")
public @interface FastTest {
}
@Test
@FastTest
void test_annotation() {
    System.out.println("local 환경에서 테스트 fast");
}

@Test
@SlowTest
void test_annotation2() {
    System.out.println("ci 환경에서 테스트 slow");
}

테스트 반복하기

@DisplayName("반복 테스트")
@RepeatedTest(value = 10, name = "{currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) {
    System.out.println("test" + repetitionInfo.getCurrentRepetition() + repetitionInfo.getTotalRepetitions());
}

@ParameterizedTest(name = "{index} / {0}")
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요."})
@NullAndEmptySource
void parameterizedTest(String message){
    System.out.println("message = " + message);
}

테스트 인스턴스

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

JUnit은 테스트 메소드 마다 테스트 인스턴스를 새로 만든다.

  • 이것이 기본 전략.
  • 테스트 메소드를 독립적으로 실행하여 예상치 못한 부작용을 방지하기 위함이다.
  • 이 전략을 JUnit 5에서 변경할 수 있다.

@TestInstance(Lifecycle.PER_CLASS)

  • 테스트 클래스당 인스턴스를 하나만 만들어 사용한다.
  • 경우에 따라, 테스트 간에 공유하는 모든 상태를 @BeforeEach 또는 @AfterEach에서 초기화 할 필요가 있다.
  • @BeforeAll과 @AfterAll을 인스턴스 메소드 또는 인터페이스에 정의한 default 메소드로 정의할 수도 있다.

테스트 순서

실행할 테스트 메소드 특정한 순서에 의해 실행되지만 어떻게 그 순서를 정하는지는 의도적으로 분명히 하지 않는다. (테스트 인스턴스를 테스트 마다 새로 만드는 것과 같은 이유)

경우에 따라, 특정 순서대로 테스트를 실행하고 싶을 때도 있다. 그 경우에는 테스트 메소드를 원하는 순서에 따라 실행하도록 @TestInstance(Lifecycle.PER_CLASS)와 함께 @TestMethodOrder를 사용할 수 있다.
MethodOrderer 구현체를 설정한다.
기본 구현체

  • Alphanumeric
  • OrderAnnoation
  • Random
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)

@Order(1)

junit-platform.properties

JUnit 설정 파일로, 클래스패스 루트 (src/test/resources/)에 넣어두면 적용된다.

테스트 인스턴스 라이프사이클 설정
junit.jupiter.testinstance.lifecycle.default = per_class

확장팩 자동 감지 기능
junit.jupiter.extensions.autodetection.enabled = true

@Disabled 무시하고 실행하기
junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition

테스트 이름 표기 전략 설정
junit.jupiter.displayname.generator.default =
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

확장 모델

JUnit 4의 확장 모델은 @RunWith(Runner), TestRule, MethodRule.
JUnit 5의 확장 모델은 단 하나, Extension.

확장팩 등록 방법

  • 선언적인 등록 @ExtendWith
  • 프로그래밍 등록 @RegisterExtension
  • 자동 등록 자바 ServiceLoader 이용

확장팩 만드는 방법

  • 테스트 실행 조건
  • 테스트 인스턴스 팩토리
  • 테스트 인스턴스 후-처리기
  • 테스트 매개변수 리졸버
  • 테스트 라이프사이클 콜백
  • 예외 처리
  • ….
package me.khjzzm.thejavatest;

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.lang.reflect.Method;

public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final long THRESHOLD = 1000L;

    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {
        ExtensionContext.Store store = store(context);
        store.put("START_TIME", System.currentTimeMillis());
    }


    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        Method requiredTestMethod = context.getRequiredTestMethod();
        String testMethodName = requiredTestMethod.getName();
        SlowTest annotation = requiredTestMethod.getAnnotation(SlowTest.class);
        ExtensionContext.Store store = store(context);
        long start_time = store.remove("START_TIME", long.class);
        long duration = System.currentTimeMillis() - start_time;
        if(duration > THRESHOLD && annotation == null){
            System.out.printf("Please consider mark method [%s] with @SlowTest.\n", testMethodName);
        }
    }


    private static ExtensionContext.Store store(ExtensionContext context) {
        String testClassName = context.getRequiredTestClass().getName();
        String testMethodName = context.getRequiredTestMethod().getName();
        ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));
        return store;
    }

}
@SlowTest
@Tag("slow")
@DisplayName("Extension")
void findSlowTestExtension_pass() throws InterruptedException {
    Thread.sleep(1005L);
    System.out.println("ci 환경에서 테스트 slow");
}

@Test
@Tag("slow")
@DisplayName("Extension")
void findSlowTestExtension() throws InterruptedException {
    Thread.sleep(1005L);
    System.out.println("ci 환경에서 테스트 slow");
}