IT/언어

[JUnit] JUnit5의 테스트 클래스 작성

개발자 두더지 2023. 1. 31. 21:19
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

JUnit5이란


 JUnit5 User Guide에는 다음과 같이 기재되어 있다.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

 각각에 대해 간단히 설명하면 다음과 같다.

JUnit Platform 테스트 실행을 위한 플랫폼과 같은 부분
JUnit Jupiter JUnit5의 테스트를 쓰기 위한 API나 테스트 엔진
JUnit Vintage JUnit3, JUnit4의 테스트 엔진

 JUnit4에서는 모든 기능이 하나의 패키지에 들어 있었지만. JUnit5에서는 분할되어 있다.

 JUnit5에서는 필요에 따라 JAR 파일을 의존관계에 포함시킬 필요가 있다. JUnit4으로 쓰여진 테스트를 실행하는 경우는 junit-vintage-engine가 필요하다. 현시점에서는 experimental의 기능도 다른 패키지에서 제공되어 있다.

 

 

JUnit5을 사용한 테스트


 JUnit5의 특징을 살린 테스트를 소개하도록 하겠다.

 이번에는 JSON등을 오브젝트로 변환하는 Jackson의 테스트 클래스를 부분적으로 작성했다.

 테스트에서는 ObjectMapperTester이라는 인터페이스에 기재한 내용이 JSON, YAML, XML의 경우에 각각 실행되도록 돼어있다.

 

테스트 클래스

//ObjectMapperTest.java

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.MappingJsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import models.Employee;
import org.junit.jupiter.api.*;
 
import java.io.IOException;
import java.nio.file.Paths;
import java.util.stream.Stream;
 
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
 
@DisplayName("ObjectMapper의 테스트")
class ObjectMapperTest {
 
    interface ObjectMapperTester {
 
        ObjectMapper createObjectMapper();
 
        String getFilePath();
 
        JsonFactory expectedJsonFactory();
 
        @TestFactory
        @DisplayName("readValue로 File에서 Employee오브젝트로 변환되는 것")
        default Stream<DynamicNode> readValueByFile() throws IOException {
            ObjectMapper mapper = createObjectMapper();
            Employee employee = mapper.readValue(Paths.get(getFilePath()).toFile(), Employee.class);
            return Stream.of(
                    dynamicTest("id가 1일것",
                            () -> assertEquals(1, employee.getId())),
                    dynamicTest("name가 GMO일것",
                            () -> assertEquals("GMO", employee.getName())),
                    dynamicTest("age가 24일것",
                            () -> assertEquals(24, employee.getAge()))
            );
        }
 
        @Test
        @DisplayName("getFactory에서 기대한 내용을 획득할 수 있는 것")
        default void getFactory() {
            ObjectMapper mapper = createObjectMapper();
            assertEquals(expectedJsonFactory().getClass(), mapper.getFactory().getClass());
        }
 
    }
 
    @Nested
    @DisplayName("JSON의 경우")
    class Json implements ObjectMapperTester {
 
        @Override
        public ObjectMapper createObjectMapper() {
            return new ObjectMapper();
        }
 
        @Override
        public String getFilePath() {
            return "src/test/resources/json/employee.json";
        }
 
        @Override
        public JsonFactory expectedJsonFactory() {
            return new MappingJsonFactory();
        }
 
    }
 
    @Nested
    @DisplayName("YAML의 경우")
    class Yaml implements ObjectMapperTester {
 
        @Override
        public ObjectMapper createObjectMapper() {
            return new YAMLMapper();
        }
 
        @Override
        public String getFilePath() {
            return "src/test/resources/yaml/employee.yaml";
        }
 
        @Override
        public JsonFactory expectedJsonFactory() {
            return new YAMLFactory();
        }
 
    }
 
    @Nested
    @DisplayName("XML의 경우")
    class Xml implements ObjectMapperTester {
 
        @Override
        public ObjectMapper createObjectMapper() {
            return new XmlMapper();
        }
 
        @Override
        public String getFilePath() {
            return "src/test/resources/xml/employee.xml";
        }
 
        @Override
        public JsonFactory expectedJsonFactory() {
            return new XmlFactory();
        }
 
    }
 
}

테스트에서 사용한 Employee 클래스

//Employee.java

package models;
 
import lombok.Data;
 
@Data
public class Employee {
    private int id;
    private String name;
    private int age;
}

테스트에서 사용한 데이터

employee.json

{
  "id": 1,
  "name": "GMO",
  "age": 24
}

employee.yaml

id: 1
name: "GMO"
age: 24

employee.xml

<employee>
    <id>1</id>
    <name>GMO</name>
    <age>24</age>
</employee>

 

테스트 클래스 해설

 위의 테스트 클래스에서는 다음과 같은 JUnit5의 특징을 담았다.

Test Interfaces and Default Methods

 인터페이스 @Test등을 붙인 default 메소드를 정의하는 것으로 인터페이스를 정의한 클래스로 테스트를 실행할 수 있게 된다.

 아까 봤던 테스트 클래스에서는 변환전의 데이터형식마다 내부 클래스를 작성하여, 인터페이스를 구현했다. 각각의 내부 클래스에서는 인터페이스에 정의된 데이터 형식마다의 ObjectMapper을 획득하는 메소드나, expected의 값을 획득하는 메소드 등을 오버라이드하고 있다.

 인터페이스에 테스트를 기재하는 것으로, 테스트를 공통화할 수 있으므로, 테스트 코드의 양을 줄일 수 있다.

 또한, default메소드에 @BeforeAll, @BeforeEach, @AfterAll, @AfterEach등의 어노테이션을 붙이는 것으로 인터페이스를 정의한 클래스로의 전처리, 후처리를 정의할 수 있다.

Dynamic Tests

 JUnit5 에서는 테스트를 동적으로 실행하는 것이 가능하다. Dynamic Test는 @TestFactory를 붙인 Stream을 반환하는 메소드를 이용해 정의하는 것이 가능하다. 

 테스트 데이터를 동적으로 생성하는 경우 등에 활용하 수 있는듯하다.

 방금봤던 테스트 클래스에서는 Dynamic Test의 메리트를 충분히 활용하고 있지 않을 수도 있지만. JUnit 5를 사용한 테스트 클래스에서는 활용하는 기회가 많을 것이라고 생각된다.

Nested Tests

 JUnit 5에서는 테스트를 네스트할 수 있다. @Nested를 붙인 내부 클래스에 따라 정의할 수 있다. 위에서의 테스트 클래스에서도 테스트 케이스마다 내부 클래스를 정의하고, 테스트 케이스를 나눈 것이 명확하게 되어있음을 확인할 수 있다.

 케이스마다 줄바꿈하는 것으로 테스트 내용이 알기 쉽게 된다.

Display Names

 @DisplayName을 사용하여, 테스트 클래스에 설명을 기재할 수 있다. 기재한 내용이 테스트 실행시에 표시된다.


참고자료

https://techblog.gmo-ap.jp/2018/04/04/junit-5%E3%81%A7%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%82%AF%E3%83%A9%E3%82%B9%E4%BD%9C%E6%88%90/

728x90