Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

Junit 5 본문

Junit 의 구성

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


시작하기

기본 애노테이션

  • @Test
  • @BeforeAll / @AfterAll
  • @BeforeEach / @AfterEach
  • @Disabled

테스트 이름 표시

@DisplayNameGeneration

  • Method와 Class 레퍼런스를 사용해서 테스트 이름을 표기하는 방법 설정.
  • 기본 구현체로 ReplaceUnderscores 제공

@DisplayName

  • 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 애노테이션.
  • @DisplayNameGeneration 보다 우선 순위가 높다.
//전체 적용 : 메소드 명의 언더스코어를 공백으로 치환
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class InitTest {

    @Test
    @DisplayName("스터디 객체 만들기")
    void create_new_study() {
        Init study = new Init();
        assertNotNull(study);
        System.out.println("InitTest.create_new_study");
    }

    @Test
    @Disabled //깨지는 테스트에 활용
    void create_new_study_again() {
        System.out.println("InitTest.create_new_study_again");
    }

    //전체 테스트를 실행하기 전에 한번 실행
    @BeforeAll
    static void setUpBeforeClass() throws Exception {
        System.out.println("StudyTest.setUpBeforeClass");
    }

    //전체 테스트를 실행한 후에 한번 실행한다
    @AfterAll
    static void tearDownAfterClass() throws Exception {
        System.out.println("StudyTest.tearDownAfterClass");
    }

    //각각의 테스트를 실행하기 전에 실행
    @BeforeEach
    void setUp() throws Exception {
        System.out.println("StudyTest.setUp");
    }

    //각각의 테스트를 실행한 후에 실행
    @AfterEach
    void tearDown() throws Exception {
        System.out.println("StudyTest.tearDown");
    }

}

 


Assertion

JUnit이 제공하는 기능

org.junit.jupiter.api.Assertions.*

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

마지막 매개변수로 실패 시 출력할 메세지 작성 가능. Supplier<String> 타입의 인스턴스를 람다 형태로 제공할 수 있다.

  • 복잡한 메시지 생성해야 하는 경우 사용하면, lazy하게 실패한 경우에만 해당 메시지를 만들게 할 수 있다.
//마지막 인자 : 실패하면 메시지 출력. Supplier로 만들면 실패할 때 lazy하게 생성 가능
() -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면 상태값이 " + StudyStatus.DRAFT + "여야 한다");
assertTrue(study.getLimitCount() > 0, () -> "스터디 최대 참석 인원은 0보다 커야 한다.");

 

assertAll을 쓰면  틀린 항목 모두 로그에 다 남겨준다. (원래는 앞에서 틀리면 멈춤)

 

Assertions 외 AssertJ, Hemcrest, Truth 등의 라이브러리를 사용할 수도 있다.


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

아래 기능을 사용하면, 특정 OS, 환경변수, JRE 등에 따라 테스트 실행 여부를 결정할 수 있다.

  • org.junit.jupiter.api.Assumptions.*
  • @Enabled___ 와 @Disabled___
    • OnOS
    • OnJre
    • IfSystemProperty
    • IfEnvironmentVariable
    • If

태깅과 필터링

테스트 메소드에 @Tag 애노테이션을 붙여 그룹을 만들고 실행 시 필터링 할 수 있다.

@Test
@DisplayName("스터디 객체 만들기 fast")
@Tag("fast")
void create_new_study_fast() {
    System.out.println("금방 끝나니까 LOCAL에서 돌려도 된다");
}

@Test
@DisplayName("스터디 객체 만들기 slow")
@Tag("slow")
void create_new_study_slow() {
    System.out.println("오래걸리니까 LOCAL에서 돌리지 말고 CI 환경에서 돌리자");
}

 

인텔리J에서 특정 태그로 테스트 필터링하도록 설정

 

커스텀 태그

JUnit 5 애노테이션을 조합하여 커스텀 태그를 만들 수 있다.

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

테스트 반복하기

@RepeatedTest

  • 반복 횟수와 반복 테스트 이름을 설정할 수 있다.
  • RepetitionInfo 타입의 인자를 받을 수 있다.
@RepeatedTest(value = 10, name = "맨 앞은 안되네 {displayName}, {currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) {
    System.out.println("StudyTest.repeatTest " + repetitionInfo.getCurrentRepetition()
            + "/" + repetitionInfo.getTotalRepetitions());
}

 

@ParameterizedTest

  • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행한다.
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName}, message={0}}")
@NullAndEmptySource
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요"})
void parameterizedTest(String message) {
    System.out.println("message = " + message);
}

 

인자 값들의 소스

  • @ValueSource
  • @NullSource, @EmptySource, @NullAndEmptySource    ▷ null, empty 넣어줌
  • @EnumSource
  • @MethodSource
  • @CsvSource      CSV : Comma-Separated Values
  • @CvsFileSource
  • @ArgumentSource
@DisplayName("@MethodSource 테스트")
@Nested
class MethodSourceTest {

    @DisplayName("String::length 메서드가 문자열의 길이를 반환한다.")
    @ParameterizedTest(name = "length of \"{0}\" is {1}")
    @MethodSource("stringAndLengthProvider")
    void testWithStringProvider(String argument, long length) {
        assertEquals(length, argument.length());
    }

    static Stream<Arguments> stringAndLengthProvider() {
        return Stream.of(
            Arguments.of("apple", 5),
            Arguments.of("banana", 6)
        );
    }
}

 

※ 외부 패키지의 메소드를 참조하는 것도 가능

 

인자 값 타입 변환 - 파라미터로 객체를 받고 싶을 때

  • 암묵적인 타입 변환
  • 명시적인 타입 변환 
    • 인자가 하나      SimpleArgumentConverter 상속 받은 구현체 제공, @ConvertWith
    • 인자가 여러개 ArgumentsAggregator 인터페이스 구현, @AggregateWith
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName}, study={0}}")
//    @NullAndEmptySource // Null, empty 값 추가
@ValueSource(ints = {10, 20, 30})
void parameterizedTest(@ConvertWith(StudyConverter.class) Study study) {
    System.out.println("study = " + study.getLimit());
}

static class StudyConverter extends SimpleArgumentConverter {
    @Override
    protected Study convert(Object source, Class<?> targetType) throws ArgumentConversionException {
        assertEquals(Study.class, targetType, () -> "Can only convert to Study");
        return new Study(Integer.parseInt(source.toString()));
    }
}

@DisplayName("스터디 만들기 CSV")
@ParameterizedTest(name = "{index} {displayName}, study={0}}")
@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTestCsv(@AggregateWith(StudyAggregator.class) Study study) {
    System.out.println(study);
}

//제약 조건 : static class 여야 한다.
static class StudyAggregator implements ArgumentsAggregator {
    @Override
    public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext parameterContext) throws ArgumentsAggregationException {
        return new Study(accessor.getInteger(0), accessor.getString(1));
    }
}

 


테스트 인스턴스

JUnit은 기본적으로 테스트 메소드 마다 테스트 인스턴스를 새로 만든다

테스트 메소드를 독립적으로 실행하여 예상치 못한 부작용을 방지하기 위함이다.

 

Junit5에서 인스턴스 생성 전략을 바꾸는 방법

@TestInstance(Lifecycle.PER_CLASS)

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

 

@Nested

  • 테스트 클래스 안에 테스트 클래스를 넣을때

테스트 순서

기본적으로 테스트는 순서를 보장하지 않는다. (보통 순서대로 실행되긴 하지만)

 

유즈 테스트, 시나리오 테스트 등에서는 테스트 간 상태를 공유하고 의존성이 필요할 수도 있다.

이처럼 상태를 공유하고 특정 순서대로 테스트를 실행하고 싶은 경우엔

@TestInstance(Lifecycle.PER_CLASS)와 함께 @TestMethodOrder를 사용할 수 있다.

  • 순서 설정을 위해 MethodOrderer 구현체를 설정한다.
    기본 구현체 : 
    • Alphanumeric
    • OrderAnnoation
    • Random
@TestInstance(TestInstance.Lifecycle.PER_CLASS) //상태 공유
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) //순서
class StudyTest {

    int value = 0; //상태 공유를 위함

    @Order(1)
    void first() {
    	value ++;
    }
    
    @Order(2)
    void second() {
    	if(value == 1)
        	value--;
    }
}

 


JUnit 설정 : junit-platform.properties

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

IDE에서 인식 못 할 경우 설정

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

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

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

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

 


확장 모델

JUnit 4의 확장 모델은 @RunWith(Runner), TestRule, MethodRule.

JUnit 5의 확장 모델은 단 하나, Extension.

 

확장팩 등록 방법

  • 선언적인 등록 @ExtendWith   <- 객체가 아니라 클래스 단위라 테스트 별 설정 값 변경 불가
  • 프로그래밍 등록 @RegisterExtension   <- 객체 단위라 테스트 별 설정 값 변경 가능
  • 자동 등록 자바 ServiceLoader 이용  <- 비추. 원치않는 Extension이 들어올 수 있으니 명시적으로 사용하자.
//@ExtendWith(FindSlowTestExtension.class)
class StudyTest {

    @RegisterExtension
    static FindSlowTestExtension findSlowTestExtension = new FindSlowTestExtension(1000L);

 

 

확장팩 만드는 방법

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

확장팩을 만들어 붙여보자

ExtensionContext를 통해 클래스명, 메소드명, 저장소를 가져올 수 있다.

public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    //    private static final long THRESHOLD = 1000L;
    private long THRESHOLD;

    public FindSlowTestExtension(long THRESHOLD) {
        this.THRESHOLD = THRESHOLD;
    }

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

    @Override
    public void afterTestExecution(ExtensionContext extensionContext) throws Exception {
        SlowTest annotation = extensionContext.getRequiredTestMethod().getAnnotation(SlowTest.class);

        ExtensionContext.Store store = getStore(extensionContext);
        Long startTime = store.remove("START_TIME", long.class);
        long duration = System.currentTimeMillis() - startTime;
        if (duration > THRESHOLD && annotation == null) {
            System.out.printf("<<<Please consider mark method [%s] with @SlowTest>>>\n", extensionContext.getRequiredTestMethod().getName());
        }
    }

    private ExtensionContext.Store getStore(ExtensionContext extensionContext) {
        //ExtensionContext 에는 값들을 저장할 수 있는 store 가 있다.
        String testClassName = extensionContext.getRequiredTestClass().getName();
        String testMethodName = extensionContext.getRequiredTestMethod().getName();
        ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));
        return store;
    }
}

JUnit 4 마이그레이션

junit-vintage-engine을 의존성으로 추가하면, JUnit 5의 junit-platform으로 JUnit 3과 4로 작성된 테스트를 실행할 수 있다.

 

변경 사항

JUnit 4 JUnit 5
@Category(Class) @Tag(String)
@RunWith, @Rule, @ClassRule @ExtendWith, @RegisterExtension
@Ignore @Disabled
@Before, @After,
@BeforeClass, @AfterClass
@BeforeEach, @AfterEach,
@BeforeAll, @AfterAll

※ @RunWith(SpringRunner.class)는 @ExtendWith({SpringExtension.class})로 바꿔야 하는데, @SpringBootTest에 포함되어 있다.


참고 자료 & 이미지 출처
더 자바, 애플리케이션을 테스트하는 다양한 방법 (백기선 님)

 

'Test > 애플리케이션을 테스트하는 다양한 방법' 카테고리의 다른 글

Web MVC / Security / JPA 관련 test  (0) 2024.12.15
Mockito  (0) 2024.11.10
테스트 - DB 연동  (0) 2024.08.17
테스트의 종류  (0) 2021.05.01