Junit5

Unit Test란?

Unit Test는 Class, Method 등에서 발생할 수 있는 오류들을 작은 단위의 Test Case로 나누어 검사하는 방법입니다. 또한 Unit Test는 외부 의존성을 배제하고 독립적으로 테스트 되어야 합니다.

Unit Test의 장점

  • 작은 단위로 나누어 오류를 정확하고 빠르게 파악할 수 있다.
  • 개발시간에서 차지하는 디버깅 시간을 단축 시킨다.
  • 테스트 코드를 믿고 리팩토링을 자주 할 수 있기 때문에 코드 품질이 향상 된다.
  • 운영 중인 코드에 버그 또는 추가 개선 사항이 있을 때 최소한의 검증은 테스트 코드가 해준다.

JUnit5 (Jupiter)

  • JUnit5는 Unit Test를 쉽게할 수 있도록 만들어진 Test Framework의 한 종류인 JUnit의 최신 버전이다.

  • JUnit5는 JUnit Platform + JUint Jupiter + JUnit Vintage의 Test Framework이다.

  • Junit Platform은 테스트를 발견하고 테스트 계획을 생성하는 TestEngine 인터페이스를 가지고 있고, JUint Jupiter나 JUnit Vintage는 TestEngine 인터페이스에 대한 구현체로서 작성한 테스트 코드를 발견하고 실행합니다.

    image-20210320235416135

JUnit4는 Vintage engine을 사용하며 spring boot 2.2.x 버전부터 기본 engine이 jupiter로 변경 됨.

JUnit4, JUnit5는 Switch하여 테스트 하기는 쉽다. 테스트 파일을 생성할 때 Testing Libraray를 JUnit4를 선택하면 gradle이 해당하는 라이브러리를 다운받는다. (gradle에 직접 명시해도 된다.)

product Class에서 바로 Test Class를 생성하는 방법도 있다. Product Class에서 Alt+Ins, Test…을 선택하면 된다.

image-20210320235004914

image-20210320233827378

이렇게 하면 gradle 파일에 junit 4버전이 추가된다.

image-20210320233217003

image-20210320232943586

물론 이 상태에서 JUnit5도 테스트가 가능하다.

image-20210320233429807

같은 패키지내에 JUnit 4와 JUnit 5를 동시에 구현해서 실행하면 Engine 명도 표시된다.

image-20210320234255320

JUnit5의 life Cycle는 상세 이미지를 참고하고, 간단하게 설명 하자면 아래와 같다.

exectue -> hooks( beforeXXX ) -> Test methods -> hooks( afterXXX ) -> result

Annotation:

@Test 테스트 메서드

@Test
@DisplayName("기본적인 테스트 메소드")
void test1() {
	Assertions.assertEquals(1, 1);
}

@ParameterizedTest 매개 변수가 있는 테스트.

@ParameterizedTest
@DisplayName("매개변수 테스트")
@ValueSource(strings={"Java", "Spring", "JUnit5"})
public void test(String param) {
	Assertions.assertTrue(() -> {
		return Objects.requireNonNull(param).length() > 3; 
    });
}

image-20210321174123909

@RepeatedTest 반복 테스트를 위한 테스트 .

@RepeatedTest(3)
@DisplayName("반복 테스트")
void testMethod1() {
    Assertions.assertEquals(1, 1);
}

@RepeatedTest(3)
void testMethod2(RepetitionInfo repetitionInfo) {
    Assertions.assertTrue(repetitionInfo.getCurrentRepetition() <= repetitionInfo.getTotalRepetitions());
}

@RepeatedTest(value = 3, name="{displayName} {currentRepetition} of {totalRepetitions}")
@DisplayName("JUnit5 반복 테스트")
void testMethod3(TestInfo testinfo) {
    Assertions.assertTrue(Objects.nonNull(testinfo.getDisplayName()));
}

@TestFactory 동적 테스트.

@SpringBootTest
public class SampleDynamicTest {

    @TestFactory
    Collection<DynamicTest> dynamicTests() {
        return Arrays.asList(
            dynamicTest("java", () -> assertTrue(true)),
            dynamicTest("spring", new CustomExecutable()),
            dynamicTest("Exception", () -> {throw new RuntimeException("Runtime Exception Test.");})
        );
    }
}

class CustomExecutable implements Executable {
    @Override
    public void execute() throws Throwable {
        System.out.println("~~~ ok ~~~~");
    }
}

@TestTemplate

메서드가 등록 된 공급자가 반환 한 호출 컨텍스트 수에 따라 여러 번 호출되도록 설계된 테스트 케이스 의 템플릿.

@TestMethodOrder (JUnit4 @FixMethodOrder)

테스트 메서드에 @Order를 사용하려면 해당 클래스에 이 어노테이션을 정의해야 함.

@SpringBootTest(classes = SampleTestMethodOrder.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class SampleTestMethodOrder {
    @Test
    @Order(3)
    @DisplayName("첫번째 메소드")
    public void firstMethod() {
        Assertions.assertEquals(1, 1);
    }

    @Test
    @Order(1)
    @DisplayName("두번째 메소드")
    public void secondMethod() {
        Assertions.assertEquals(1, 1);
    }
}

@BeforeEach (JUnit4 @Before)

각 메서드 실행 전 실행되는 메서드

@AfterEach (JUnit4 @After)

각 메서드 실행 후 실행되는 메서드

@BeforeAll (JUnit4 @BeforeClass)

클래스 내에서 가장 먼저 실행되는 메서드.

@AfterAll

클래스내에서 마지막으로 실행되는 메서드

@TestInstance

클래스에 대한 테스트 인스턴스 수명주기 를 구성하는 데 사용됩니다.

PER_CLASS

  • Test 클래스 당 하나의 인스턴스가 생성.
  • @BeforeAll, @AfterAll 함수를 static 없이 사용 가능함.

PER_CLASS

  • Test 메서드 당 하나의 인스턴스가 생성.
@SpringBootTest(classes = SampleTestInstance.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SampleTestInstance {
    @BeforeAll
    public void beforeAllTest() {
        System.out.println(">>> beforeAllTest called");
    }

    @BeforeEach
    public void beforeEachTest() {
        System.out.println(">>> beforeEachTest called");
    }

    @Test
    void test1() {
        System.out.println(">>> test1 called");
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test2() {
        System.out.println(">>> test2 called");
        Assertions.assertEquals(1, 1);
    }

    @AfterEach
    public void afterEachTest() {
        System.out.println(">>> afterEachTest called");
    }

    @AfterAll
    public void afterAllTest() {
        System.out.println(">>> afterAllTest called");
    }
}

@DisplayName

테스트 클래스 또는 메서드에 대한 사용자 지정 표시 이름을 선언합니다.

@Test
@DisplayName("테스트 이름")
public void testMethod() {
	Assertions.assertEquals(1, 1);
}

@DisplayNameGeneration

클래스에 대한 사용자 지정 표시 이름 생성기 를 선언합니다.

Class에 Annotation을 작성.

@DisplayName보다 우선순위가 낮음.

Standard Matches the standard display name generation behavior in place since JUnit Jupiter 5.0 was released.
Simple Removes trailing parentheses for methods with no parameters.
ReplaceUnderscores Replaces underscores with spaces.
IndicativeSentences Generates complete sentences by concatenating the names of the test and the enclosing classes.
@SpringBootTest(classes=SampleDisplayGeneration.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class SampleDisplayGeneration {

    @Test
    void test_if_equals() {
        Assertions.assertEquals(1,1);
    }

    @Test
    @DisplayName("DisplayName 우선 적용")
    void test_if_true() {
        Assertions.assertTrue(true);
    }
}

image-20210322165405274

@Nested

중첩 테스트 클래스 임을 나타냅니다 .

여러 테스트 그룹 간의 관계를 표현할 수 있는 더 많은 기능을 테스트 작성자에게 제공하고, 테스트 구조에 대한 계층적 사고를 용이하게 합니다. 내부 클래스만 @Nested 테스트 클래스로 사용할 수 있으며 @BeforeAll, @AfterAll 메서드는 직접 사용할 수 없습니다. 그 이유는 Java가 내부 클래스에 정적 멤버를 허용하지 않기 때문입니다. 그러나 @Nested 클래스에 @TestInstance(Lifecycle.PER_CLASS)를 사용하면 이러한 제한을 피할 수 있습니다.

package com.sample.junit;

import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes=SampleNested.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SampleNested {

    @BeforeAll
    void beforeAllTest() {
        System.out.println("beforeAllTest called!");
    }

    @Test
    @DisplayName("테스트1 ")
    void test1() {
        Assertions.assertEquals(1, 1);
    }

    @Nested
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class InnerNestedTest1 {

        @BeforeAll
        void innerBeforeAllTest() {
            System.out.println("innerBeforeAllTest called!");
        }

        @Test
        @DisplayName("중첩테스트 1")
        public void innerTest1() {
            Assertions.assertEquals(1, 1);
        }

        @Test
        @DisplayName("중첩테스트 2")
        public void innerTest2() {
            Assertions.assertEquals(1, 1);
        }
    }

    @Nested
    class InnerNestedTest2 {
        @Test
        @DisplayName("중첩테스트 3")
        void test3() {
            Assertions.assertEquals(1, 1);
        }
    }
}

@Tag (JUnit @Categories)

클래스 또는 메서드 수준에서 테스트 필터링을위한 태그 를 선언하는 데 사용.

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("moduleA")
@Test
public @interface ModuleA {
}

ModuleA 처럼 ModuleB, ModuleC Custom Annotation를 만든 후 아래와 같은 코드를 작성한다.

package com.example.tdd1;

import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class SampleDefault {
    UserDto user;
    
    @BeforeEach
    public void beforeEachTest() {
        //각 @Test Method가 실행되기 전 실행 된다.
        user = UserDto.builder().userId("Java").userName("World").build();
    }

    @ModuleA
    @DisplayName("assertTrue 테스트")
    public void assertTrueTest() {
        Assertions.assertTrue(user.getUserId().equalsIgnoreCase("java"), "userId가 java이다.");

    }
    
    @ModuleB
    @DisplayName("assertFalse 테스트")
    void assertFalseTest() {
        Assertions.assertFalse(user.getUserName().length() < 5, "userName이 5자리 보다 작다.");
    }

    @ModuleC
    @DisplayName("assertEquals 테스트")
    void assertEqualsTest() {
        Assertions.assertEquals(user.getUserId(), "Java");
        Assertions.assertEquals(user.getUserName(), "World", "userName이 World가 아니다.");
    }
}

Tags 필터링 방법

Test를 IntelliJ IDEA로 할 경우 Settings-Build…-Build Tools-Gradle에서 Run tests using을 IntelliJ IDEA로 선택하고 Run/Debug Configuration에서 Tags를 선택하고 Expression을 입력한다.

image-20210321164718225

image-20210321165223878

image-20210321165908669

한 가지 주의할 점은 IntelliJ 단축키 Ctrl+Shift+F10으로 실행할 경우 설정한 Tags가 실행되는게 아니라 해당 클래스가 Test Run되기 때문에 우측 상단에 설정한 Tags인지 확인 해야 한다. 그래서 설정한 Tags로 계속 테스트할 때는 Shift+F10 단축키로 실행해야 한다.

image-20210321170152274

image-20210321170232396

Gradle로 Tags를 설정하여 Test하는 방법은 Settings-Build…-Build Tools-Gradle에서 Run tests using을 Gradle로 하고 build.gradle에 아래 내용을 추가한다.

test {
    useJUnitPlatform {
        includeTags 'moduleA', 'moduleB'
        // excludeTags 'moduleC'
        includeEngines 'junit-jupiter'
        // excludeEngines 'junit-vintage'
    }
}

그리고 Run Configurations에서 Gradle을 추가하고 설정 정보를 넣은 다음 실행하면 된다.

image-20210321170916528

image-20210321171028397

개인적으로 IntelliJ IDEA를 선호하는데 그 이유는 결과 창에 상세 항목 정보가 안나오고 실행된 수만 나오기 때문이다. (위 그림에서 보라색 Gradle Icon을 클릭하면 웹에서 상세 정보를 볼 수는 있지만…)

@Disabled (JUnit @Ignore)

테스트 클래스 또는 메서드 를 비활성화. (테스트 제외)

@Test
@Disabled
@DisplayName("기본적인 테스트 메소드")
void test1() {
    Assertions.assertEquals(1, 1);
}

image-20210322211225392

@Timeout

실행이 주어진 기간을 초과하는 경우 테스트, 테스트 팩토리, 테스트 템플릿 또는 수명주기 메서드를 실패하는 데 사용. (기본 시간 단위는 초로 설정되어 있고 변경 가능함.)

@Timeout Annotation이 달린 메서드는 주 스레드에서 진행되고, 시간 제한을 초과하면 다른 스레드에서 주 스레드를 중단합니다. @Timeout Annotation을 @Nested Class에 달면 클래스 내 모든 테스트, 테스트 팩토리 및 테스트 템플릿 메서드에 적용됩니다.

Parameter value Equivalent annotation
42 @Timeout(42)
42 ns @Timeout(value = 42, unit = NANOSECONDS)
42 μs @Timeout(value = 42, unit = MICROSECONDS)
42 ms @Timeout(value = 42, unit = MILLISECONDS)
42 s @Timeout(value = 42, unit = SECONDS)
42 m @Timeout(value = 42, unit = MINUTES)
42 h @Timeout(value = 42, unit = HOURS)
42 d @Timeout(value = 42, unit = DAYS)
@Test
@Timeout(1)
void test1() {
    Assertions.assertEquals(1, 1);
}

boolean asynchronousResultNotAvailable() {
    return true;
}

@Test
@Timeout(5)
void pollUntil() throws InterruptedException {
    while (asynchronousResultNotAvailable()) {
        Thread.sleep(250);
    }
}

image-20210322214655471

@ExtendWith

확장을 선언적 으로 등록하는 데 사용.

package com.sample.junit;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = SampleExtendWith.class)
@ExtendWith(Extendsion1.class)
public class SampleExtendWith {

    @Test
    void test_SampleExtendWith_1() {
        System.out.println("test_SampleExtendWith_1 called!");
        Assertions.assertEquals(1, 1);
    }
}

class Extendsion1 implements BeforeEachCallback, AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        System.out.println("Extendsion1 beforeEach called!");
    }

    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        System.out.println("Extendsion1 afterEach called!");
    }
}

image-20210322221428446

@RegisterExtension

필드를 통해 프로그래밍 방식으로 확장 을 등록하는 데 사용 .

@RegisterExtension
static WebServerExtension server = WebServerExtension.builder()
    .enableSecurity(false)
    .build();

@Test
void getProductList() {
    WebClient webClient = new WebClient();
    String serverUrl = server.getServerUrl();
    // Use WebClient to connect to web server using serverUrl and verify response
    assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}

@TempDir

라이프 사이클 방법 또는 테스트 방법에서 필드 주입 또는 매개 변수 주입을 통해 임시 디렉토리 를 제공하는 데 사용.

@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
    Path file = tempDir.resolve("test.txt");

    new ListWriter(file).write("a", "b", "c");

    assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}

JUnit 병렬 실행

image-20210322225304741

기본적으로 JUnit은 단일 쓰래드에서 순차적으로 실행됩니다. 테스트 속도를 높이기 위해 병렬로 실행하길 원한다면 JUnit5(5.3버전 이후) 옵션 기능으로 사용할 수 있습니다.

src/test/resources/junit-platform.properties 생성. (resoureces디렉토리 및 설정 파일 생성)

Ctrl+Shift+Alt+S 프로젝트 설정에서 resources 추가.

image-20210322225709291

병렬 테스트를 위한 코드

package com.sample.junit;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = SampleParallelTest.class)
public class SampleParallelTest {

    @Test
    void test1() throws InterruptedException {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test2() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test3() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test4() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test5() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test6() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test7() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test8() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }

    @Test
    void test9() throws InterruptedException  {
        Thread.sleep(1000);
        Assertions.assertEquals(1, 1);
    }
}

단일 쓰레드 실행 결과

image-20210322230032572

junit-platform.properties파일에 아래 내용 넣고 실행

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

image-20210322230234853

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent

image-20210322230413414

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread

image-20210322230523788

@ResourceLock

Resources`: `SYSTEM_PROPERTIES`, `SYSTEM_OUT`, `SYSTEM_ERR`, `LOCALE`, or `TIME_ZONE
package com.sample.junit;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;

/**
 * SAME_THREAD: Force execution in same thread as the parent node.
 * CONCURRENT : Allow concurrent execution with any other node.
 */
@Execution(ExecutionMode.CONCURRENT)
public class SampleResourceLock {

    @Test
    @ResourceLock(value = "SYSTEM_PROPERTIES", mode = ResourceAccessMode.READ)
    void test1() {
        Assertions.assertNull(System.getProperty("my.prop"));
    }

    @Test
    @ResourceLock(value = "SYSTEM_PROPERTIES", mode = ResourceAccessMode.READ_WRITE)
    void test2()  {
        System.setProperty("my.prop", "apple");
        Assertions.assertEquals("apple", System.getProperty("my.prop"));
    }

    @Test
    @ResourceLock(value = "SYSTEM_PROPERTIES", mode = ResourceAccessMode.READ_WRITE)
    void test3() {
        System.setProperty("my.prop", "banana");
        Assertions.assertEquals("banana", System.getProperty("my.prop"));
    }
}

콜백함수

image-20210322234219182

image-20210322234333722

image-20210322234403319

JUnit5 User Guide : https://junit.org/junit5/docs/current/user-guide

댓글남기기