Java

JUnit5를 사용해서 자바 애플리케이션을 테스트 하는 방법

마닐라 2022. 3. 18. 19:01

백기선님의 인프런 강의를 듣고 정리하여 쓴 글입니다.

 

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

단위 테스트를 작성하는 자바 개발자 93%가 JUnit을 사용합니다.

 

JUnit5는 그 자체로 크게 3가지로 모듈화되어 있습니다.

PlatForm : IDE를 통해서 테스트를 실행해주는 런처를 제공합니다. TestEngine API를 제공합니다.

Jupiter : TestEngine의 구현체로 JUnit5를 제공합니다.

Vintage : JUnit 4와 3을 지원하는 TestEngine의 구현체입니다.

 

기본 어노테이션

기본 애노테이션은 아래와 같습니다.

  • @Test
  • @BeforeAll / @AfterAll
  • @BeforeEach / @AfterEach
  • @Disabled
package me.whiteship.inflearnthejavatest;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class StudyTest {

    @Test
    void create() {
        Study study = new Study();
        assertNotNull(study);
        System.out.println("create");
    }

    @Test
    //@Disabled // 전체 테스트 진행 시 제외
    void create1() {
        System.out.println("create1");
    }

    //모든 테스트 이전에 한번만 실행
    @BeforeAll
    static void beforeAll() {
        System.out.println("before all");
    }

    //모든 테스트 이후에 한번만 실행
    @AfterAll
    static void afterAll() {
        System.out.println("after all");
    }

    //각 테스트 이전에 실행
    @BeforeEach
    void beforeEach() {
        System.out.println("Before each");
    }

    //각 테스트 이후에 실행
    @AfterEach
    void AfterEach() {
        System.out.println("After each");
    }

}

테스트 실행 결과

 

 

테스트 이름 표시하기

테스트의 이름을 표시하는 어노테이션도 있습니다. 기본 표기는 메서드 이름이 됩니다.

 

@DisplayNameGeneration

  • Method와 Class 레퍼런스를 사용해서 테스트 이름을 표기하는 방법 설정.
  • 기본 구현체로 ReplaceUnderscores 제공(언더바스코어를 공백으로 바꿔주어 가독성이 좋아집니다.)

@DisplayName

  • 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 애노테이션.
  • @DisplayNameGeneration 보다 우선 순위가 높습니다.
  • 공백을 바로 넣을 수 있고 이모지도 넣을 수 있습니다..!

참고: https://junit.org/junit5/docs/current/user-guide/#writing-tests-display-names

 

 

Assertion

검증하고자 하는 내용을 확인할 때 Assesions 클래스를 사용합니다.

위치 : org.junit.jupiter.api.Assertions.*

 

실제 값이 기대한 값과 같은지 확인 assertEquals(expected, actual)
값이 null이 아닌지 확인 assertNotNull(actual)
다음 조건이 참(true)인지 확인 assertTrue(boolean)
모든 확인 구문 확인 assertAll(executables...)
예외 발생 확인 assertThrows(expectedType, executable)
특정 시간 안에 실행이 완료되는지 확인 assertTimeout(duration, executable)
    assertEquals(StudyStatus.DRAFT, study.getStatus(),
            "스터디를 처음 만들면 " + StudyStatus.DRAFT + "상태다.");
}
    assertEquals(StudyStatus.DRAFT, study.getStatus(), 
            () -> "스터디를 처음 만들면 " + StudyStatus.DRAFT + "상태다.");
}

 

assertEquals 메서드는 세번째 인자로 에러 메세지를 넣을 수가 있는데, 두 가지 형태 모두 작성 가능하지만 람다식 형태로 작성하면 실패했을 때에만 문자열 연산을 하기 때문에 연산의 비용을 줄일 수 있습니다.

 

테스트가 2가지 있다고 할 때 먼저 실행되는 테스트가 깨졌을 때 두번째 테스트의 통과여부는 확인하지 않습니다.

모든 연관된 테스트를 한번에 실행하는 방법이 있습니다.

그 때 사용하는 것이 assertAll 메서드입니다.

assertAll(
        () -> assertNotNull(study),
        () -> assertEquals(StudyStatus.DRAFT, study.getStatus(),
                "스터디를 처음 만들면 " + StudyStatus.DRAFT + "상태다."),
        () -> assertTrue(study.getLimit() > 0, "스터디 최대 참석 가능 인원은 0보다 커야 한다.")
);

 

assertAll 안에 테스트 하고자하는 케이스들을 람다식으로 넣으면 한번에 실행됩니다.

실패한 2개의 테스트 확인

테스트 코드를 실행할 때 예외가 발생하는지 확인하고 싶을 땐 assertThrows() 메서드를 사용합니다.

assertThrows(IllegalArgumentException.class, () -> new Study(-10));

 

해당 메세지가 내가 기대했던 메세지와 같은지도 확인할 수 있습니다.

public Study(int limit) {
    if(limit < 0) {
        throw new IllegalArgumentException("limit은 0보다 커야한다.");
    }
    this.limit = limit;
}
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Study(-10));
String message = exception.getMessage();
assertEquals("limit은 0보다 커야한다.", exception.getMessage());

 

아래의 코드는 코드가 0.1초안에 끝나지 않을 때 예외가 발생합니다.

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

테스트 결과

테스트가 모두 끝나기까지 가디리지 않고 0.1초안에 실행이 안되면 테스트를 그냥 종료하고 싶을 때가 있을겁니다.

그럴때는 assertTimeoutPreemptively 메서드를 사용합니다.

동일하게 코드는 실패하지만 테스트 실행에 걸린 시간이 줄어든 것을 확인할 수 있습니다. 

 

하지만 이 메서드는 주의해서 사용해야 합니다.

assertTimeoutPreemptively 테스트 코드에서 코드 블럭은 별도의 쓰레드에서 실행되기 때문에

만약에 쓰레드 로컬을 사용하는 코드가 안에 있다면 예상치 못한 결과가 발생할 수 있습니다.

예를 들어 스프링 트랜잭션 처리의 경우 쓰레드 로컬을 기본 전략으로 사용하는데,

쓰레드 로컬은 다른 쓰레드와 공유가 되지 않습니다.

트랜잭션 설정이 해당 테스트에서 제대로 적용되지 않을 수 있다는 것을 의미합니다.

테스트 코드는 롤백을 기본으로 실행을 하는데 롤백이 되지않고 DB에 반영이 되어버리는 일이 발생할 수 있으므로 주의해서 사용해야합니다.

 

조건에 따라 선택적으로 테스트 실행하기

특정한 조건을 만족하는 경우에 테스트를 실행하는 방법이 있습니다.

 

org.junit.jupiter.api.Assumptions.*

  • assumeTrue(조건)
  • assumingThat(조건, 테스트)

@Enabled___ 와 @Disabled___

  • OnOS
  • OnJre
  • IfSystemProperty
  • IfEnvironmentVariable
  • If

 

assumeTrue("LOCAL".equalsIgnoreCase(System.getenv("TEST_ENV")));

 

위의 코드는 시스템의 "TEST_ENV"라는 환경변수가 LOCAL이 맞는지 확인하는 테스트 코드입니다.

해당 테스트가 실패한 다면 테스트의 다음 라인은 실행되지 않습니다.

 

 assumingThat을 이용하면 조건에 따라 다른 테스트를 실행시킬 수 있습니다.

String test_env = System.getenv("TEST_ENV");

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

assumingThat("inseong".equalsIgnoreCase(test_env), () -> {
    Study actual = new Study(10);
    assertThat(actual.getLimit()).isGreaterThan(0);
});

 

@Enabled와 @Disabled도 비슷한 기능을 합니다.

@Test
@DisplayName("스터디 만들기 \uD83D\uDE31")
@EnabledOnOs({OS.WINDOWS, OS.LINUX})
void create_new_study() {
    String test_env = System.getenv("TEST_ENV");

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

    assumingThat("inseong".equalsIgnoreCase(test_env), () -> {
        Study actual = new Study(10);
        assertThat(actual.getLimit()).isGreaterThan(0);
    });

}

@Test
@DisabledOnOs(OS.MAC)
    //@Disabled // 전체 테스트 진행 시 제외
void create_new_study_again() {
    System.out.println("create1");
}

 

OS와 특화된 테스트가 있다면 위와 같이 @EnabledOnOs로 간단하게 작성할 수 있습니다.

 

@Test
@DisplayName("스터디 만들기 \uD83D\uDE31")
@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10, JRE.JAVA_11})
void create_new_study() {
    String test_env = System.getenv("TEST_ENV");

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

    assumingThat("inseong".equalsIgnoreCase(test_env), () -> {
        Study actual = new Study(10);
        assertThat(actual.getLimit()).isGreaterThan(0);
    });

}

 

JRE 버전에 따른 테스트도 위와 같이 @EnabledOnJre로 작성하면 됩니다.

 

@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "local")
void create_new_study() {
    String test_env = System.getenv("TEST_ENV");
}

 

환경변수에 매칭되는 문자열에 따른 테스트도 위와 같이 @EnabledIfEnvironmentVariable을 사용할 수 있습니다.

 

태깅과 필터링

태깅은 테스트들을 그룹화할 수 있는 기능을 말합니다.

실행하는 데에 시간이 오래걸려서 로컬에 실행하기에는 부담이 되는 경우 서버에서 빌드할 때 실행되도록 할 수도 있습니다.

테스트 시 태그를 붙여 필터링하기

 

@Test
@DisplayName("스터디 만들기 fast")
@Tag("fast")
void create_new_study() {}

@Test
@DisplayName("스터디 만들기 slow")
@Tag("slow")
void create_new_study_again() {}

 

위와 같이 태그 fast에 대해 필터링 되도록 설정하면 "fast"가 붙은 태그에 대해서만 테스트를 진행할 수 있습니다.

하지만 빌드할 때는 태그 관계 없이 모든 테스트가 다 실행됩니다.

이런 경우 빌드를 따로 세팅 해주어야합니다.

 

<profiles>
    <profile>
        <id>default</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <groups>fast</groups>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

pom.xml 설정 파일에서 위와 같이 세팅을 하면 빌드를 하더라도 "fast"가 붙은 테스트만 실행하게 됩니다.

 

서버에서는 빌드가 되도록 하려면 아래와 같이 프로파일을 추가해주면 됩니다.

<profile>
    <id>ci</id>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <groups>fast | slow</groups>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

 

위와 같이 적게 되면 fast나 slow 태그가 붙은 테스트들을 빌드 시 모두 실행하게 됩니다.

그냥 모두 실행 시키려면 configuration 태그를 빼버리면 됩니다.

 

 

groups에 들어가는 태그들에 대한 자세한 표현식은 아래 사이트에서 참고할 수 있습니다.

 

커스텀 태그

애노테이션을 조합하여 커스텀 태그를 만들 수 있습니다.기존 코드는 아래와 같습니다.

@Test
@DisplayName("스터디 만들기 fast")
@Tag("fast")
void create_new_study() {}

@Test
@DisplayName("스터디 만들기 slow")
@Tag("slow")
void create_new_study_again() {}

 

애노테이션을 조합하여 만든 애노테이션입니다.

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

 

해당 애노테이션을 사용하면 아래와 같이 간결하게 사용할 수 있습니다.

@FastTest
@DisplayName("스터디 만들기 fast")
void create_new_study() {}

@SlowTest
@DisplayName("스터디 만들기 slow")
void create_new_study_again() {}

 

테스트 반복하기

테스트를 매번 실행할 때 마다 랜덤값을 쓴다던가 타이밍에 따라 달라질 수 있는 조건이 있을 때 간단하게 반복시킬 수 있습니다.

 

@RepeatedTest

  • 반복 횟수와 반복 테스트 이름을 설정할 수 있습니다.
  • RepetitionInfo 타입의 인자를 받을 수 있습니다.

@ParameterizedTest

  • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행합니다.

 

@RepeatedTest(3)
void repeatTest(RepetitionInfo repetitionInfo) {
    System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" +
            repetitionInfo.getTotalRepetitions());
}

 

단순하게 @RepeatedTest 애노테이션을 사용하면 테스트 횟수를 지정할 수 있습니다.

 

@DisplayName("스터디 만들기")
@RepeatedTest(value = 3, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) {
    System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" +
            repetitionInfo.getTotalRepetitions());
}

 

반복 횟수 뿐 아니라 반복되는 테스트 별로 이름을 지정할 수도 있습니다. 결과는 아래와 같습니다.

@RepeatedTest 애노테이션 사용

 

@ParameterizedTest를 사용하면 각 파라미터 값에 따른 테스트를 진행할 수 있습니다.

 

@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요."})
void parameterizedTest(String message) {
    System.out.println(message);
}

@ParameterizedTest, @ValueSource 애노테이션 사용

 

위에서 @ParameterizedTest에 대해서 @ValueSource 애노테이션이 사용되었습니다.

같이 사용가능한 애노테이션의 종류들은 아래와 같습니다.

 

인자 값들의 소스

  • @ValueSource
  • @NullSource, @EmptySource, @NullAndEmptySource
  • @EnumSource
  • @MethodSource
  • @CsvSource
  • @CsvFileSource
  • @ArgumentSource

 

인자 값 타입 변환

  • 암묵적인 타입 변환
  • 명시적인 타입 변환

 

인자 값 조합

  • ArgumentsAccessor
  • 커스텀 Accessor

 

참고 : https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

 

 

@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(ints = {10, 20, 40})
void parameterizedTest(Study study) {
    System.out.println(study.getLimit());
}

 

위와 같이 하나의 숫자 타입을 Study 타입으로 변환하고자 할 때 custom 구현체를 따로 만들 수도 있습니다.

 

@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(ints = {10, 20, 40})
void parameterizedTest(@ConvertWith(StudyConverter.class) Study study) {
    System.out.println(study.getLimit());
}

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

 

Study 클래스에 대한 Argument를 하나하나씩 받아와서 처리할 수 있습니다.

@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTest(Integer limit, String name) {
    Study study = new Study(limit, name);
    System.out.println(study);
}

 

이것도 역시 Study Argument 자체로 받아오는 방식이 있습니다.

하지만 SimpleArgumentConverter는 하나의 Argument에 대한 것입니다.

이럴 때는 ArgumentAccess를 사용할 수 있습니다.

@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTest(ArgumentsAccessor argumentsAccessor) {
    Study study = new Study(argumentsAccessor.getInteger(0), argumentsAccessor.getString(1));
    System.out.println(study);
}

 

위의 방법도 있고 커스텀 arggregator를 만드는 방법도 있습니다.

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

static class StudyAggregator implements ArgumentsAggregator {

    @Override
    public Object aggregateArguments(ArgumentsAccessor argumentsAccessor, ParameterContext parameterContext) throws ArgumentsAggregationException {
        Study study = new Study(argumentsAccessor.getInteger(0), argumentsAccessor.getString(1));
        return study;
    }
}

 

Aggregator에 대한 제약조건이 하나 있는데 반드시 static inner class 이거나 public class으로 구현해야 합니다.

 

테스트 인스턴스

JUnit은 테스트 메소드 마다 클래스 인스턴스를 새로 만들고 이것을 기본 전략으로 사용하고 있습니다.

 

int value = 1;

@FastTest
@DisplayName("스터디 만들기 fast")
void create_new_study() {
    System.out.println(value++);
}

@SlowTest
@DisplayName("스터디 만들기 slow")
void create_new_study_again() {
    System.out.println(value++);
}

 

테스트 마다 새로운 인스턴스를 만든다고 했으므로 1이라는 값이 2번 출력됩니다.

 

@FastTest
@DisplayName("스터디 만들기 fast")
void create_new_study() {
    System.out.println(this);
    System.out.println(value++);
}

@SlowTest
@DisplayName("스터디 만들기 slow")
void create_new_study_again() {
    System.out.println(this);
    System.out.println(value++);
}

 

실행 결과

 

해시 코드값을 출력해보면 각각 다른 해시값을 가지고 있다는 것을 확인할 수 있습니다.

이렇게 만든 이유는 테스트 간의 의존성을 없애기 위해서입니다.

JUni5는 선언된 순서로 실행을 하지만 매번 그럴 것이라고 예상을 하면 안됩니다.

그렇기 때문에 테스트 간에 의존성을 없게 만드는 것이 좋습니다.

 

이러한 기본 전략을 변경할 수도 있습니다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)을 사용하면 되는데 이 경우 테스트 클래스당 인스턴스를 하나만 만들어 사용합니다.

경우에 따라 테스트 간 공유하는 모든 상태를 @BeforeEach 또는 @AfterEach에서 초기화할 필요가 있습니다.

 

애당 어노테이션을 붙인 뒤에 위의 코드를 동일하게 실행시키면 아래와 같은 결과가 나옵니다.

 

해시값이 동일해지고 해당 인스턴스에 대한 변수를 공유하므로 출력도 2가 됩니다.

 

테스트 순서

내부적으로 정해진 순서가 있긴 하나 그 순서에 의존해서는 안됩니다.

어떤 순서에 의해서 실행이 된다고 명확히 하지 않은 이유는 제대로 작성된 단위 테스트라면 다른 단위 테스트와 독립적으로 실행되어야하기 때문입니다.

하지만 경우에 따라서는 원하는 순서대로 테스트를 작성하고 싶을 때가 있습니다.

ex) 회원가입 -> 로그인 -> 개인 페이지 활동 같은 테스트

그럴 경우에는 @TestInstance(TestInstance.Lifecycle.PER_CLASS)와 함께 @TestMethodOrder()를 사용할 수 있습니다.

인자로 MethodOrderer 구현체를 설정하여 넘기는데 기본 구현체는 3가지가 있습니다.

  • Alphanumeric
  • OrderAnnoation
  • Random

 

@Order(2)
@FastTest
@DisplayName("스터디 만들기 fast")
void create_new_study() {
    System.out.println(this);
    System.out.println(value++);
}

@Order(1)
@SlowTest
@DisplayName("스터디 만들기 slow")
void create_new_study_again() {
    System.out.println(this);
    System.out.println(value++);
}

 

위와 같이 메서드에 @Order(숫자)를 붙이면 낮은 숫자의 메서드가 우선순위를 갖게 됩니다.

@TestInstance와 @TestMethodOrder 두가지를 같이 꼭 써야하는 것은 아니지만 상태 정보도 공유하고 순차적인 실행을 원할 때 같이 사용하면 좋습니다.

 

junit-platform.properties

해당 파일은 JUnit 설정 파일로 클래스 패스 루트(src/test/resources/)에 넣어두면 적용됩니다.

모든 테스트 클래스에 대해서 적용하고 싶을 때 사용합니다.

 

테스트 인스턴스 라이프사이클 설정

junit.jupiter.testinstance.lifecycle.default = per_class

 

확장팩 자동 감지 기능(기본값은 false)

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 5 확장 모델

JUnit 4의 확장 모델은 @RunWith(Runner), TestRule, MethodRule 가 있었지만

JUnit 5의 확장 모델은 Extension 하나만 존재합니다.

 

확장팩 등록 방법은 아래와 같습니다.

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

확장팩 만드는 방법

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

참고 : https://junit.org/junit5/docs/current/user-guide/#extensions

 

public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    private static final long THRESHOLD = 1000L;
    @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 {
        String testMethodName = extensionContext.getRequiredTestMethod().getName();
        ExtensionContext.Store store =  getStore(extensionContext);
        Long start_time = store.remove("START_TIME", long.class);
        long duration = System.currentTimeMillis() - start_time;
        if(duration > THRESHOLD) {
            System.out.printf("Please consider mark method [%s] with @SlowTest.\n", testMethodName);
        }
    }

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

 

메서드 실행시 1초가 넘어가면 문자를 출력해주는 클래스를 만들었습니다.

해당 클래스를 기존 테스트 클래스에 적용해보겠습니다.

 

@ExtendWith(FindSlowTestExtension.class)

 

클래스 상단에 @ExtendWith 키워드를 붙여 선언적인 등록을 합니다.

 

@Order(1)
@SlowTest
@DisplayName("스터디 만들기 slow")
void create_new_study_again() throws InterruptedException {
    Thread.sleep(1005L);
    System.out.println(this);
    System.out.println(value++);
}

 

그리고 의도적을 1초이상이 소요되도록 테스트 메서드를 작성했습니다.

 

 

그 결과 'Please consider mark method [create_new_study_again] with @SlowTest. 라는 메세지가 정상적으로 출력되는 것을 볼 수 있습니다.

 

하지만 이미 slow라는 태그가 붙어있는 테스트에 대해서는 출력할 필요가 없습니다.

아까 만들었던 FindSlowTestExtension 클래스에서 해당 애노테이션 아닐 때만 출력하도록 변경할 수 있습니다.

 

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

    String testMethodName = extensionContext.getRequiredTestMethod().getName();
    ExtensionContext.Store store =  getStore(extensionContext);

    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);
    }
}

실행 대상이 되는 테스트 메서드를 불러와서 slow이 애노테이션이 없는 경우 출력하도록 할 수 있습니다.

 

위에서 확장한 FindSlowTestExtension 클래스는 slow의 기준이 되는 THRESHOLD의 값을 1000으로 고정해놓았습니다.

하지만 slow의 기준을 테스트마다 다르게 하고 싶을 수도 있습니다.

생성자를 만들어서 전달해줄 수 있으나 테스트 클래스의 @ExtendWith 애노테이션을 붙이는 방식으로는 생성자에 값을 전달할 방법이 없습니다.

 

그러한 문제를 해결 하기 위해 프로그래밍 등록 방식인 @RegisterExtension을 사용할 수 있습니다.

이 방법은 필드로 정의하는 방식입니다.

클래스 상단에 애노테이션을 붙이지 말고 아래과 같이 필드에 @RegisterExtension 애노테이션을 사용합니다.

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

이와 같은 방식으로 정의를 하면 생성자를 통해서 slow의 기준이 되는 THRESHOLD의 값을 지정해서 넘겨줄 수 있습니다.

 

이 방법말고 Extension을 의존성에만 추가해놓고 ServiceLoader를 이용해서 자동으로 등록되게끔 할 수도 있습니다.

기본적으로 이 기능은 꺼져있기때문에 위에서 보았던 Junit5 설정파일인 junit-platform.properties에서 아래 설정을 true로 변경해주면 됩니다.

 

확장팩 자동 감지 기능(기본값은 false)

junit.jupiter.extensions.autodetection.enabled = true

 

특정한 포맷에 맞춰서 넣어야하는데 자세한 내용은 아래의 사이트에서 참고할 수 있습니다.

웬만하면 명시적으로 사용하는게 좋아보입니다.

참고 : https://junit.org/junit5/docs/current/user-guide/#extensions-registration-automatic

 

 

JUnit 4 마이그레이션

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

 

@Rule은 기본적으로 지원하지 않지만, junit-jupiter-migrationsupport 모듈이 제공하는 @EnableRuleMigrationSupport를 사용하면 다음 타입의 Rule을 지원합니다.

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

 

<exclusions>
    <exclusion>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
</exclusions>

Junit4를 사용하려면 위와 같이 junit-vintage-engine을 의존성으로 추가합니다.

 

import org.junit.Before;
import org.junit.Test;

public class StudyJUnit4Test {
    @Before
    public void before() {
        System.out.println("before");
    }
    @Test
    public void createTest1() {
        System.out.println("test1");
    }
    @Test
    public void createTest2() {
        System.out.println("test2");
    }
}

추가하게 되면 위와 같이 JUnit4 기반으로 테스트 코드를 작성할 수 있습니다.

맨 처음에서 봤듯이 JUnit 5가 제공하는 JUnit Platform을 가지고 Vintage 구현체가 JUnit4 스타일의 코드를 지원하는 겁니다.

 

전체 테스트를 실행해보면 버젼별로 나눠서 출력해주는 것을 볼 수 있습니다.

JUnit4, JUni5 테스트 전체 실행