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

JUnit

JUnit

JUnit 5: 소개

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

  • https://www.jetbrains.com/lp/devecosystem-2019/java/
    • 단위 테스트를 작성하는 자바 개발자 93% JUnit를 사용한다.
  • 자바 8 이상을 필요로 한다.
  • 대체제 : TestNG, Spock 등등
  • Platform : 테스트를 실행해주는 런처 제공, TestEngine API 제공
  • Jupiter: TestEngine API 구현체로 JUnit 5 를 제공
  • Vintage: JUnit 4 와 2 을 지원하는 TestEngine 구현체
  • JUnit 4는 하나의 덩어리 jar로 들어오고 junit 이 참조하는 다른 라이브러리가 있다.
  • JUnit 5는 그 차체로 모듈화가 되어있다.(Platform, Jupiter, Vintage)

참고

JUnit 5 : 시작하기

  • Test 메소드에 public 없어도 실행 가능하다.
    • 리플렉션 사용하기 때문에

스프링 부트 프로젝트 만들기

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

스프링 부트 프로젝트 사용하지 않는다면


<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
</dependency>

기본 애노테이션

  • @Test
  • @BeforeAll
    • 여러 테스트가 모두 실행이 될 때 모든 테스트를 실행하기 전에 한 번만 실행
    • 반드시 static 메서드 사용 private 은 안되고 default는 됨 static void 기억하면 된다.
  • @AfterAll
    • 여러 테스트가 모두 실행이 될 때 모든 테스트를 실행한 후에 한 번만 실행
    • 반드시 static 메서드 사용 private 은 안되고 default는 됨 static void 기억하면 된다.
  • @BeforeEach
    • 여러 테스트가 모두 실행이 될 때 각각의 테스트 실행하기 전에 실행
    • static 일 필요 없다.
  • @AfterEach
    • 여러 테스트가 모두 실행이 될 때 각각의 테스트 실행한 후에 실행
    • static 일 필요 없다.
  • @Disabled
    • 테스트 중에 실행 안 하기 설정 메서드

JUnit 5 : 테스트 이름 표시하기

  • 기본 표기 전략은 메서드 이름 표시

@DisplayNameGeneration

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

@DisplayName

  • 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 어노테이션
  • @DisplayNameGeneration 보다 우선 순위가 높다.

소스


@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {

    @Test
    @DisplayName("스터디 만들기 ╯°□°)╯")
    void create_new_study() {
        Study study = new Study();
        assertNotNull(study);
        System.out.println("create");
    }

    @Test
    @DisplayName("스터디 만들기 \uD83D\uDE31")
    void create_new_study_again() {
        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");
    }

}

참고

JUnit 5 : Assertion

org.junit.jupiter.api.Assertions.*

  • 실제 값이 기대한 값과 같은지 확인
    • assertEquals(expected, actual)
  • 값이 null 아닌지 확인
    • assertNotNull(actual)
  • 다음 조건이 참(true)인지 확인
    • assertTrue(boolean)
  • 모든 확인 구문 확인
    • assertAll(executables…)
  • 예외 발생 확인
    • assertThrows(expectedType, executable)
  • 특정 시간 안에 실행이 완료되는지 확인
    • assertTimeout(duration, executable)
    • assertTimeout 설정한 시간을 기다렸다가 비교하고 종료한다
    • assertTimeoutPreemptively 설정한 시간을 기다리지 않고 종료한다
      • 주의해서 사용해야함 ThreadLocal 을 사용하는 코드가 있다면 예상치 못한 결과가 나타날수 있다.
      • 스프링 transaction 은 ThreadLocal 을 기본 전략으로 사용하는데 ThreadLocal 은 설정을 안 하면 다른 스레드에서 공유를 할 수 없다 그때는 스프링 transaction 설정이 제대로 적용이 안될 수 있다 테스트에서 스프링은 transaction 한 테스트를 rollback 을 기본으로 하는데 롤백이 안되고 db에 반영되는 경우가 발생할 수 있다.
  • 마지막 매개변수로 Supplier 타입의 인스턴스를 람다 형태로 제공할 수 있다,
    • 복잡한 메시지 생성해야 하는 경우 사용하면 실패한 경우에만 해당 메시지를 만들게 할 수 있다,
  • AssertJ, Hemcrest, Truth 등의 라이브러리를 사용할 수도 있다.

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

특정 조건을 만족하는 경우에 테스트를 실행하는 방법

  • org.junit.jupiter.api.Assumptions.*
    • assumeTrue(조건)
    • assumingThat(조건, 테스트)

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {

    @Test
    @DisplayName("스터디 만들기 ╯°□°)╯")
    void create_new_study() {
        assumeTrue("Local".equalsIgnoreCase(System.getenv("TEST_ENV")));
        Study actual = new Study(100);
        assertThat(actual.getLimit()).isGreaterThan(0);
    }

    @Test
    @DisplayName("스터디 만들기 \uD83D\uDE31")
    void create_new_study_again() {

        assumingThat("Local".equalsIgnoreCase(System.getenv("TEST_ENV")), () -> {
            System.out.println("create1");
        });

    }

}

  • @Enable___ 와 @Disabled___
    • OnOS
    • OnJre
    • IfSystemProperty
    • IfEnvironmentVariable
    • If

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {

    @Test
    @DisplayName("스터디 만들기 ╯°□°)╯")
    @EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "LOCAL")
    void create_new_study() {
        Study actual = new Study(100);
        assertThat(actual.getLimit()).isGreaterThan(0);
    }

    @Test
    @DisplayName("스터디 만들기 \uD83D\uDE31")
    @EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "test")
    void create_new_study_again() {

        System.out.println("create1");
    }

    @Test
    @EnabledOnOs({OS.MAC, OS.LINUX})
    void test(){
        System.out.println("test");
    }

    @Test
    @EnabledOnJre({JRE.JAVA_8, JRE.JAVA_11})
    void test3(){
        System.out.println("test3");
    }

    @Test
    @DisabledOnOs({OS.MAC, OS.LINUX})
    void test2(){
        System.out.println("test2");
    }

    @Test
    @DisabledOnJre(JRE.OTHER)
    void test4(){
        System.out.println("test2");
    }

}

JUnit 5 : 태깅과 필터링

테스트 그룹을 만들고 원하는 테스트 그룹만 테스트를 실행할 수 있는 기능.

  • @Tag
    • 테스트 메소드에 태그를 추가할 수 있다.
    • 하나의 테스트 메소드에 여러 태그를 사용할 수 있다.

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {

    @Test
    @DisplayName("스터디 만들기 fast")
    @Tag("fast")
    void create_new_study() {
        Study actual = new Study(100);
        assertThat(actual.getLimit()).isGreaterThan(0);
    }

    @Test
    @DisplayName("스터디 만들기 slow")
    @Tag("slow")
    void create_new_study_again() {
        System.out.println("create1");
    }

}

  • 인텔리 J 에서 특정 태그로 테스트 필터링 하는 방법
    • edit configuration -> test kind -> Tags 로 설정 후 Tag 이름 지정
  • 메이븐에서 테스트 필터링 하는 방법

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <groups>fast | slow</groups>
    </configuration>
</plugin>


<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>
        <profile>
            <id>ci</id>
            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-surefire-plugin</artifactId>
                    </plugin>
                </plugins>
            </build>
        </profile>
</profiles>


참고

JUnit 5 : 커스텀 태그

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

  • FastTest.java

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


@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {

  @FastTest
  @DisplayName("스터디 만들기 fast")
  void create_new_study() {
    Study actual = new Study(100);
    assertThat(actual.getLimit()).isGreaterThan(0);
  }

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

}

JUnit 5 : 테스트 반복하기 1부

@RepeatedTest

  • 반복 횟수와 반복 테스트 이름을 설정할 수 있다.
    • {displayName}
    • {currentRepetition}
    • {totalRepetitions}
  • RepetitionInfo 타입의 인자를 받을 수 있다.

@ParameterizedTest

  • JUnit 5 는 기본으로 제공
  • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행한다.
    • {displayName}
    • {index}
    • {arguments}
    • {0}, {1}, …

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
    

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

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

}

JUnit 5 : 테스트 반복하기 2부

인자 값들의 소스

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

인자 값 타입 변환

  • 암묵적인 타입 변환
  • 명시적인 타입 변환
    • SimpleArgumentConverter 상속 받은 구현체 제공
    • @ConvertWith

인자 값 조합

  • ArgumentsAccessor
  • 커스텀 Accessor
    • ArgumentsAggregator 인터페이스 구현
    • @AggregateWith

소스


@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
    
    @DisplayName("스터디 만들기")
    @ParameterizedTest(name = "{index} {displayName} message={0}")
    @CsvSource({"10, '자바 스터디'", "20, 스프링"})
    void parameterizedTest0(ArgumentsAccessor accessor) {
      Study study = new Study(accessor.getInteger(0), accessor.getString(1));
      System.out.println(study);
    }
    
    @DisplayName("스터디 만들기")
    @RepeatedTest(value = 10, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
    void repeatTest(RepetitionInfo repetitionInfo) {
        System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" +
                repetitionInfo.getTotalRepetitions());
    }

    @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 accessor, ParameterContext context) throws ArgumentsAggregationException {
            return new Study(accessor.getInteger(0), accessor.getString(1));
        }
    }

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

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

참고

JUnit 5 : 테스트 인스턴스

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

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

@TestInstance(Lifecycle.PER_CLASS)

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

소스


@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StudyTest {

    int value = 1;

    @Test
    @DisplayName("스터디 만들기")
    void create_new_study() {
        System.out.println(this);
        System.out.println(value++);
        Study actual = new Study(1);
        assertThat(actual.getLimit()).isGreaterThan(0);
    }

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

}

JUnit 5 : 테스트 순서

  • 실행할 테스트 메소드 특정한 순서에 의해 실행되지만 어떻게 그 순서를 정하는지는 의도적으로 분명히 하지 않는다.
    • 테스트 인스턴스를 테스트 마다 새로 만드는 것과 같은 이유
  • 경우에 따라, 특정 순서대로 테스트를 실행하고 싶을 때도 있다. 그 경우에는 테스트 메소드를 원하는 순서에 따라 실행하도록 @TestInstance(TestInstance.Lifecycle.PER_CLASS)와 함께 @TestMethodOrder 를 사용할 수 있다.
    • MethodOrder 를 사용할 수 있다.
    • 기본 구현체
      • Alphanumeric
      • OrderAnnotation
      • Random
  • 소스

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudyTest {

    int value = 1;

    @Order(2)
    @Test
    @DisplayName("스터디 만들기 fast")
    void create_new_study() {
        System.out.println(this);
        System.out.println(value++);
        Study actual = new Study(1);
        assertThat(actual.getLimit()).isGreaterThan(0);
    }

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

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

JUnit 5 : junit-platform.properties

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

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

  • junit.jupiter.testinstance.lifecycle.default = per_class

확장팩 자동 감지 기능

  • junit.jupiter.extensions.autodetection.enable = 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 이용

확장팩 만드는 방법

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

소스


public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private long THRESHOLD = 1000L;

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

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

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        Method requiredTestMethod = context.getRequiredTestMethod();
        SlowTest annotation = requiredTestMethod.getAnnotation(SlowTest.class);
        String testMethodName = requiredTestMethod.getName();
        ExtensionContext.Store store = getStore(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 ExtensionContext.Store getStore(ExtensionContext context) {
        String testClassName = context.getRequiredTestClass().getName();
        String testMethodName = context.getRequiredTestMethod().getName();
        return context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));
    }
    
}


@ExtendWith(FindSlowTestExtension.class) // 선언적 방법
class StudyTest {

  int value = 1;

  @RegisterExtension
  static FindSlowTestExtension findSlowTestExtension = // 프로그래밍 등록
          new FindSlowTestExtension(1000L);
  
  @Test
  void create_new_study() {
    System.out.println(this);
    System.out.println(value++);
    Study actual = new Study(1);
    assertThat(actual.getLimit()).isGreaterThan(0);
  }

  @Test
  void create_new_study_again() throws InterruptedException {
    Thread.sleep(1005L);
    System.out.println(this);
    System.out.println("create1 " + value++);
  }

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

}

참고

JUnit 5 : JUnit 4 마이그레이션

  • junit-vintage-engine 을 의존성으로 추가하면, JUnit 5 의 junit-platform 으로 JUnit 3과 4로 작성된 테스트를 실행할 수 있다.
  • @Rule 은 기본적으로 지원하지 않지만, junit-jupiter-migrationsupport 모듈이 제공하는 @EnableRuleMigrationSupport 를 사용하면 다음 타입의 Rule 을 지원한다.
    • ExternalResource
    • Verifier
    • ExpectedException
JUnit 4JUnit 5
@Category(Class)@Tag(String)
@RunWith, @Rule, @ClassRule@ExtendWith, @RegisterExtension
@Ignore@Disabled
@Before, @After, @BeforeClass, @AfterClass@BeforeEach, @AfterEach, @BeforeAll, @AfterAll

© 2020. All rights reserved.

SIKSIK