JUnit 5

기존 JUnit 4에선 Java 5 또는 이상 버전을 지원했지만 JUnit 5에선 Java 8 또는 그 이상 버전을 지원하기 시작했다. JUnit 4에선 모든 기능들이 한 모듈에 포함되어 있었지만, JUnit 5부터 모듈이 세개로 나눠졌다.

  • JUnit Platform : 테스트 프레임워크가 동작하도록 TestEngine API를 정의하는 모듈
  • JUnit Jupiter : JUnit 5에서 지원하는 새로운 기능들을 위한 모듈
  • JUnit Vintage : JUnit 5 플랫폼에서 JUnit 3나 JUnit 4 기반의 테스트를 지원하기 위한 모듈

IntelliJ에서 사용하기

intlliJ 2017.3 이상의 버전에선, 프로젝트에 사용되는 junit-platform-launcher, junit-jupiter-engine, 그리고 junit-vintage-engine JAR 파일을 API 버전에 맞게 자동으로 다운로드 받는다.

만약 다른 버전의 JUnit 5 버전을 사용하고 싶다면 다음과 같이 설정하면 된다.

// Only needed to run tests in a version of IntelliJ IDEA that bundles older versions
testRuntime("org.junit.platform:junit-platform-launcher:1.3.2")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.3.2")
testRuntime("org.junit.vintage:junit-vintage-engine:5.3.2")

Gradle 4.6 버전 이상에선 JUnit Platform 테스트를 실행하기 위해 native support를 제공한다. 이를 활성화하기 위해선 build.gradletest 블록에서 useJUnitPlatform() 메서드를 호출하면 된다.

test {
    userJUnitPlatform()
}

자세한 Gradle 설정 : https://docs.gradle.org/current/userguide/java_testing.html#using_junit5

새로운 기능들

JUnit 5에서 Java 8 또는 그 이상버전을 지원하기 시작하면서 Java 8의 람다와 같은 새로운 기능들에 대한 장점을 수용하려 노력했다.

Annotation

JUnit 4 JUnit 5 Description
@Before @BeforeEach 테스트 메서드가 시작전에 호출되는 메서드에 사용
@After @AfterEach 테스트 메서드가 끝난 후에 호출되는 메서드에 사용
@BeforeClass @BeforeAll 모든 테스트 메서드가 호출되기 전에 호출되는 메서드에 사용
@AfterClass @AfterAll 모든 메스트 메서드가 끝난 후에 호출되는 메서드에 사용
@Ignore @Disable 테스트 클래스 또는 메서드를 실행하지 않을 때 사용
@RunWith @ExtendWith Extension을 등록할 때 사용
  @Nested Nested Test를 위해 이너 클래스에 사용
  @Tag 테스트 필터링을 위해 클래스 또는 메서드에 사용
  @TestFactory dynamic tests를 위한 테스트 팩토리 메서드에 사용
  @DisplayName 테스트 클래스 또는 메서드에 커스텀 네임을 설정할 때 사용

Assertions

Assertions이 모듈이 분리 되면서 org.junit.jupiter.api.Assertions으로 변경되었다. 뿐만 아니라 앞서 말할 것처럼 람다 표현을 지원한다.

@Test
void lambdaExpressions() {
    assertTrue(Stream.of(1, 2, 3)
              .mapToInt(i -> i)
              .sum() > 5, () -> "Sum should be greater than 5");
}

위의 예시는 간단한 표현이지만, 무거운 메세지를 사용하는 경우 lazy evaluation의 장점을 가지고 시간과 리소스를 절약하는데 도움이 된다.

또한 새롭게 assertAll() 메서드가 추가되어 여러개의 assertion에 대한 그룹을 만들어 테스트를 할 수 있다.

@Test
void groupAssertions() {
    int[] numbers = {0, 1, 2, 3, 4};
    assertAll("numbers",
        () -> assertEquals(numbers[0], 1),
        () -> assertEquals(numbers[3], 3),
        () -> assertEquals(numbers[4], 1)
    );
}

정확하게 어느 부분에서 문제가 발생하는지 알 수 있어 복잡한 assertions을 만드는데 도움이 된다.

비교

JUnit 4 JUnit 5
fail fail
assertTrue assertTrue
assertThat  
assertSame assertSame
assertNull assertNull
assertNotSame assertNotSame
assertNotEquals assertNotEquals
assertNotNull assertNotNull
assertFalse assertFalse
assertEquals assertEquals
assertArrayEquals assertArrayEquals
  assertAll
  assertThrows

Assumptions

Assumptions은 특정 조건이 만족했을 때 테스트가 수행되도록 할 때 사용된다. 일반적으로 테스트가 제대로 실행되는 데 필요한 외부 조건이 테스트와 직접 관련이 없는 경우에 사용된다.

@Test
void trueAssumption() {
    assumeTrue(5 > 1);
    assertEquals(5 + 2, 7);
}

@Test
void falseAssumption() {
    assumeFalse(5 < 1);
    assertEquals(5 + 2, 7);
}

@Test
void assumptionThat() {
    String someString = "Just a string";
    assumingThat(
        someString.equals("Just a string"),
        () -> assertEquals(2 + 2, 4)
    );
}

만약 assumptions이 실패한 경우 TestAbortedException이 발생하고 테스트를 건너뛰어진다.

비교

JUnit 4 JUnit 5
assumeFalse assumeFalse
assumeNoException  
assumeNotNull  
assumeThat assumeThat
assumeTrue assumeTrue

Exceptions

JUnit5에서 예외에 대한 테스트가 강화되었다. 새롭게 추가된 assertThrows 메서드는 예외가 발생했는지 확인하고 발생한 예외를 확인하기 위해 이를 반환한다.

@Test
void shouldThrowException() {
    Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
        throw new UnsupportedOperationException("Not supported");
    });
    assertEquals(exception.getMessage(), "Not supported");
}

Nested Tests

테스트 그룹(클래스) 간의 복잡한 관계를 표현 할 수 있게 Nested Tests가 추가되었다. 이너 클래스에 @Nestes 어노테이션을 추가하는 것으로 Nested Test를 설정할 수 있어 쉽게 이해할 수 있다.

@DisplayName("A Stack")
public class StackTest {
    
    Stack<Object> stack;
    
    @Nested
    class WhenNew {
        
        @BeforeEach
        void createInstance() {
    		stack = new Stack<>();        
        }
        
        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }
        
        @Nested
        class AfterPushing {
            
            String element = "element";
            
            @BeforeEach
            void pushAnElement() {
                stack.push(element);
            }
            
            @Test
            @DisplayName("is not empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }
        }
    }
}

Tagging

테스트 클래스 또는 메서드에 @Tag 어노테이션을 사용해 테스트를 필터링하여 실행할 수 있다.

@Tag("Test case")
public class TaggedTest {
 
    @Test
    @Tag("Method")
    void testMethod() {
        assertEquals(2 + 2, 4);
    }
}

build.gradle 파일에 다음과 같이 태그를 테스트에 포함할 것인지 아닌지를 설정하면 된다.

test {
    useJUnitPlatform {
        includeTags 'fast', 'smoke & feature-a'
        excludeTags 'slow', 'ci'
    }
}

Dynamic Test

JUnit 5에선 컴파일 시간에 고정된 테스트를 수행하는 @Test 어노테이션을 사용한 테스트뿐만 아니라 런타임에 변경이 가능한 dynamic test를 지원한다. 이런 dynamic test@TestFactory 어노테이션을 사용하는 팩토리 메서드에 의해서 런타임에 생성이 된다.

public class DynamicTestDemo {

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        return IntStream.iterate(0, n -> n + 2).limit(10)
                        .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTests() {

        Iterator<Integer> inputGenerator = new Iterator<>() {

            Random random = new Random();
            int current;

            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }

            @Override
            public Integer next() {
                return current;
            }
        };

        Function<Integer, String> displayNameGenerator = (input) -> "input : " + input;

        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);

        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }

    @TestFactory
    Stream<DynamicNode> dynamicTestsWithContainers() {
        return Stream.of("A", "B", "C")
                     .map(input -> dynamicContainer("Container " + input, Stream.of(
                             dynamicTest("not null", () -> assertNotNull(input)),
                             dynamicContainer("properties", Stream.of(
                                     dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                                     dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
                             ))
                     )));
    }
}

Display Name

JUnit 4에서 테스트를 수행한 후, 테스트 메서드 이름을 통해서 어떤 항목에 대한 테스트 결과를 확인할 수 있었다. 때문에 테스트 메서드를 작성할 때 이름을 최대한 구체적으로 작성해야 했고, 때문에 메서드 이름이 너무 길어지는 현상이 발생할 수 있었다.

@Test
public void writeNewPostWithoutAuthorizationFailsWithException() {
    // ...
}

JUnit 5에서 추가된 @DisplayName 어노테이션을 사용하면 이렇게 비대해지고 읽기 힘든 메서드 이름을 개선하면서도 테스트 결과 창에서 문장으로 구성된 직관적인 결과를 확인 할 수 있게 된다.

@Test
@DisplayName("Should throw Exception when trying to new post without authorization")
void writePostWhitoutAuth() {
    // ...
}

Migration to JUnit 5

JUnit 5로 넘어오면서 @RunWith 어노테이션은 @ExtendWith 어노테이션으로 대체되었다. JUnit 5 환경에서 @RunWith 어노테이션이 완전히 사라진 건 아니지만 이것은 이전 버전 호환성을 위해 남겨진 것이다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { SpringTestConfiguration.class })
public class GreetingsSpringTest {
    // ...
}

위의 코드를 JUnit 5를 사용하여 표현되길 원한다면 @RunWith 어노테이션을 새롭게 추가된 @ExtendWith으로 바꿔주면 된다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { SpringTestConfiguration.class })
public class GreetingsSpringTest {
    // ...
}

SpringExtension클래스는 스프링 테스트를 위해 스프링 5에 의해 제공되는 클래스이다. @ExtendWtih 어노테이션은 Extension 인터페이스를 상속받는 클래스를 value로 사용할 수 있다.

Spring Boot 2.x에서 사용하기

test {
    useJUnitPlatform()
}

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-web')
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}

위의 build.gralde 코드를 통해 JUnit 5로 테스트를 수행하려고 하면 제대로 동작하지 않는다. Spring Boot 2.x 에선 JUnit 4에 대한 의존성을 갖기 때문에 JUnit 4에 대한 의존성을 제거한 후 JUnit 5를 추가해야 한다.

test {
    useJUnitPlatform()
}

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-web')
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'junit'
    }
	testCompile('org.junit.jupiter:junit-jupiter-api:5.3.2')
	testCompile('org.junit.jupiter:junit-jupiter-params:5.3.2')
	testRuntime('org.junit.jupiter:junit-jupiter-engine:5.3.2')
}