Junit5 한번에 끝내기

소개

junit은 단위 테스트를 작성하는 자바 개발자 93%가 사용하는 테스팅 프레임워크 이다. 테스트 코드를 작성해야하는

구성

junit5

  • JUnit Platform : 테스트를 실행시켜주는 런처를 제공, TestEngine API가 제공된다.
  • Jupiter: JUnit 5를 지원하는 TestEngine API의 구연체
  • Vintage: JUnit 4, 3를 지원하는 TestEngine API의 구연체

의존성 추가

  • 2.2+ 버전의 스프링 부트 프로젝트라면 spring-boot-starter-test에 기본으로 탑제가 되어있다.
  • 아니라면…
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.5.2</version>
    </dependency>
    

기본 에노테이션은

  • @Test
  • @BeforeAll / @AfterAll : 전체 @Test 메소드 전후
  • @BeforeEach / @AfterEach : 각 @Test 메소드 전후
  • @Disabled : @Test 메소드 실행 안하기

예제

import org.junit.jupiter.api.*;

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

class StudyTest {

    @Test
    void test1() {
        System.out.println("test 1");
    }

    @Test
    void test2() {
        System.out.println("test 2");
    }

    @Test @Disabled
    void test3() {
        System.out.println("test 3");
    }

    @Test
    void create() {
        Study study = new Study();
        assertNotNull(study);
    }

    @BeforeAll
    static void beforAll() {
        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");
    }
}

결과 :

before All

before Each
after Each


before Each
test 1
after Each


before Each
test 2
after Each

after All

테스트 이름 표시

@Test
void test1() {
    System.out.println("test 1");
}

위의 코드가 있을 때 기본 이름은 메소드명 이다.

이름 표기 방법을 설정

@DisplayNameGeneration 에노테이션을 사용하는데 Class와 Method에 붙여서 테스트 이름을 표기 방법을 설정 한다. 구현체는 ReplaceUnderscores에서 제공한다.

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
    @Test
    void test_new_class() { 
        System.out.println("test 1");
    }
}

위와 같이 작성할 수 있고 테스트 이름은 test new class가 된다.

이름 설정 (추천, 이모지 사용가능)

@DisplayName 에노테이션을 Method에 붙여서 테스트 이름 설정할 수 있다. @DisplayNameGeneration보다 우선순위가 높다.

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

    @DisplayName("테스트 \uD83D\uDE00")
    @Test
    void test_new_class() { 
        System.out.println("test 1");
    }
}

test name

assert API

테스트 코드라면 당연 Assert를 써서 값이 제대로 됬는지 확인해야한다.
Java assert 예약어를 사용해도 되지만 그것보다는 좀 더 다양한 기능을 제공하는 Junit의 assert API들이 있다.
JUnit5의 org.junit.jupiter.api.Assertions.*에 다양한 Assert들이 있다.
자주 사용하는 것들만 보자면 …

  • 기본적인 assert

    assertEquals("예상하는 값", "결과", ()->"에러 메시지");
    assertNotNull(new String(), ()->"에러 메시지");
    assertNull(null, ()->"에러 메시지");
    assertTrue(1 > 0, ()->"에러 메시지");
    assertFalse(1 < 0, ()->"에러 메시지");
    

    “에러 메시지"를 굳이 람다식으로 작성하지 않아도 되지만 에러 메시지의 연산이 있을 경우
    람다식으로 작성하여 함수화 시키면 실질적으로 에러 메세지를 보여줘야 할 때만 보여준다.

  • 모든 assert 한번에 계산 assert를 사용하면 위의 assert가 False라는게 밝혀지만 아래 assert는 실행하지 않는데..
    이걸 assertAll()로 해결할 수 있다.

    assertAll(() -> {
        assertTrue(1 < 0, ()->"에러 메시지");
        assertFalse(1 > 0, ()->"에러 메시지");
    });
    
  • Exception 체크 assertThrows을 사용하면 Exception을 체크할 수 있다.

    IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
        throw new IllegalArgumentException("test");
    });
    
    assertEquals("test", exception.getMessage());
    
  • 시작 체크 assertTimeout, assertTimeoutPreemptively을 사용하면 작동되는 시간을 측정할 수 있다.
    둘중에서 assertTimeoutPreemptively을 사용하는 것을 권장한다 타입아웃이 되는 경우 assertTimeout는 종료해버리지만 assertTimeoutPreemptively는 끝내버림으로 테스트 속도향상을 할 수 있다. 단, ThreadLocal을 제대로 모른다면 쓰지마라! 특히 transaction 같은 것에

    assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
        Thread.sleep(1000);
    });
    
    assertTimeout(Duration.ofMillis(100), () -> {
        Thread.sleep(1000);
    });
    

테스트에 조건에 따라 실행

특정 OS, Java Version, 실행 변수등에 따라 실행할지 안할지 지정할 수 있다.

  • 코드로 하는 방법은 org.junit.jupiter.api.Assumptions.*의 API를 사용하면 된다.

    @Test
    void test1() {
        assumeTrue("LOCAL".equals(System.getenv("TEST_ENV")));
        assertTrue(false); //TEST_ENV값이 LOCAL이 아닐 경우 실행 안함
    }
    

    혹은

    assumingThat("LOCAL".equals(System.getenv("TEST_ENV")), () -> {
        assertTrue(false);
    });
    
  • 에노테이션으로 하는 방법은 @Enabled___@Disabled___로 시작해

    • OnOS
    • OnJre
    • IfSystemProperty
    • IfEnvironmentVariable 를 붙여서 쓰면 된다.
    @EnabledOnOs({OS.WINDOWS, OS.MAC})
    void test() {
        System.out.println("test 2");
    }
    

테스트 태깅과 필터링

태깅은 우리의 테스트에 Tag를 걸 수 있다. org.junit.jupiter.api.tag 혹은 tags를 사용하면 된다. @Tag("fast") @Test void test1() { System.out.println("fast test"); } 이렇게 Tag는 필터를 통해 특정한 테스트만 실행할 수 있다.
tag-filter
이외에는

커스텀 에노테이션

기본적은 JUnit5 jupiter 메타 에노테이션을 가지고 커스텀 에노테이션을 만들 수 있다.

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

사용할때는

@CustomeTest
void customeTest() {
    System.out.println("Custome Test");
}

단순 테스트 반복

@RepeatedTest 에노테이션을 사용하면 된다. 단순하게는

@RepeatedTest(value = 10)
void repeatedTest(RepetitionInfo repetitionInfo) {
    System.out.println(repetitionInfo.getCurrentRepetition() + " of " + repetitionInfo.getTotalRepetitions());
}

RepetitionInfo라는 메소드를 통해 우리가 반복하려는 횟수와 토탈횟수등을 받을 수 있다.

테스트를 좀 더 이쁘게 꾸미려면…

@DisplayName("반복")
@RepeatedTest(value = 10, name = "{displayName} {currentRepetition} / {totalRepetitions}")
void repeatedTest(RepetitionInfo repetitionInfo) {
    System.out.println(repetitionInfo.getCurrentRepetition() + " of " + repetitionInfo.getTotalRepetitions());
}

name{displayName}, {currentRepetition}, {totalRepetitions}에 변수를 이용해서 이름을 이쁘게 꾸밀 수 있다.

파라미터를 테스트 반복

@ParameterizedTest을 이용해서 값을 바꿔가며 테스트 할 수 있다. 단순하게는..

@ParameterizedTest
@ValueSource(strings = {"더위", "추위", "선선", "비"})
void parameterTest(String weather) {
    System.out.println("날씨: " + value);
}

테스트의 이름을 꾸미고 싶다면 다음과 같이 에노테이션을 바꾸면 된다.

@DisplayName("값 바꾸며 반복")
@ParameterizedTest(name = "{displayName} {index}...{0}, {1}")

@ValueSource말고도 다양한 에노테이션이 있다.

  • @NullSource, @EmptySource, @NullAndEmptySource
  • @EnumSource
  • @MethodSource
  • @CvsSource
  • @CvsFileSource
  • @ArgumentSource 이런 값 에노테이션에는 암묵적인 타입 변환이 있다.

테스트 인스턴스

기본적인 테스트 클래스의 인스턴스는 각 테스트 메소드마다 만들어지게 된다. (메소드가 100개면 100개..) 다음과 같은 코드를 실행하면 commonValue의 값이 2개 나와야 할거 같지만..

public class ExampleTest {

    int commonValue = 0;

    @Test
    void test1() {
        commonValue++;
        System.out.println(this + ".test1 : " + commonValue);
    }

    @Test
    void test2() {
        commonValue++;
        System.out.println(this + ".test2 : " + commonValue);
    }

}

결과는 1이 나온다.

me.sangoh.testing.ExampleTest@1c1bbc4e.test1 : 1
me.sangoh.testing.ExampleTest@247bddad.test2 : 1

왜 이런식으로 기본 동작이 각 메소드마다 인스턴스를 만드냐면 테스트 메소드를 독립적으로 실행하여 테스트 메소드간에 의존성을 제거하기 위함 이다.

테스트 인스턴스 공유하기

@TestInstance(Lifecycle.PER_CLASS) 에노테이션을 Class에 붙이면 테스트 메소드별로 생성하지 않고 하나의 인스턴스를 공유한다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ExampleTest {

    int commonValue = 0;

    @Test
    void test1() {
        commonValue++;
        System.out.println(this + ".test1 : " + commonValue);
    }

    @Test
    void test2() {
        commonValue++;
        System.out.println(this + ".test2 : " + commonValue);
    }
}

결과

me.sangoh.testing.ExampleTest@45c7e403.test1 : 1
me.sangoh.testing.ExampleTest@45c7e403.test2 : 2

테스트 순서

기본적으로 테스트 메소드의 실행 순서는 어떤 메소드가 먼저 실행될지 알 수 없다.
물론 Junit5부터는 메소드의 순서대로 작동이 되지만 하나의 메소드가 하나의 유닛테스트라면 테스트 순서에 의존해서도 신경을 써도 안된다

하지만… 그럼에도 테스트 메소드 실행 순서를 정하는 방법은 있다. 그러려면 @TestInstance(TestInstance.Lifecycle.PER_CLASS)을 사용해서 인스턴스를 공유하게 하고 @TestMethodOrder을 순서 정하는 방법을 정하고 순서를 정의하면 된다.

  • Alphanumeric
  • OrderAnnoation
  • Random
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ExampleTest {

    int commonValue = 0;

    @Order(1)
    @Test
    void test1() {
        commonValue++;
        System.out.println(this + ".test1 : " + commonValue);
    }

    @Order(2)
    @Test
    void test2() {
        commonValue++;
        System.out.println(this + ".test2 : " + commonValue);
    }
}

JUnit 설정 파일

클래스패스 루트 (src/test/resources/)에 junit-platform.properties을 넣으면 설정파일로 인식한다. 안된다고 하면… 다음 이미지를 보고 Project Test Resources를 설정하자.. resource 설정

다음과 같은 것들을 설정 할 수 있다.

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

  • 확장팩 자동 감지 기능 junit.jupiter.extensions.autodetection.enabled = true
    기본 값은 false

  • @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 static final String START_TIME = "START_TIME";

    @Override
    public void beforeTestExecution(ExtensionContext extensionContext) throws Exception {
        String testClassName = extensionContext.getRequiredTestClass().getName();
        String testMethodName = extensionContext.getRequiredTestMethod().getName();
        ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.create(testClassName + "." + testMethodName));

        store.put("START_TIME", System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext extensionContext) throws Exception {
        String testClassName = extensionContext.getRequiredTestClass().getName();
        String testMethodName = extensionContext.getRequiredTestMethod().getName();
        ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.create(testClassName + "." + testMethodName));

        Long startTime = store.remove(START_TIME, long.class);
        long duration = System.currentTimeMillis() - startTime;

        if (duration > 1000L) {
            System.out.printf("Pleas check method [%s] this method is vary slow\n", testMethodName);
        }
    }
}

선언적으로 사용하기

@ExtendWith(FindSlowTestExtension.class)
class StudyTest {

혹은 프로그래밍적으로 추가하기

class StudyTest {
    @RegisterExtension
    static FindSlowTestExtension findSlowTestExtension = new FindSlowTestExtension();

    ...test...
}

JUnit3,4를 JUnit5로 마이그래이션

JUnit 3과 4 작성된 테스트 JUnit5의 junit-platform을 사용해서 실행할 수 있다. 단 기본 기능이 아님으로 junit-jupiter-migrationsupport 의존성을 추가 해야한다.

자세한 내용은 Pass..

참고

comments powered by Disqus