져니의 개발 정원 가꾸기

(Spring)JUnit5 - 테스트코드 작성을 지원하는 프레임워크 본문

개발노트/Spring | Java

(Spring)JUnit5 - 테스트코드 작성을 지원하는 프레임워크

전전쪄니 2023. 12. 10. 18:25

목차

    스프링 프레임워크에서는 다양한 프레임워크들을 활용하여 테스트코드를 작성할 수 있다.

    업무를 하면서 JUnit 프레임워크와 Hamcrest, AssertJ라이브러리를 사용하여 다양한 단위 테스트 코드를 작성할 수 있었는데, 이들의 특징들만 알아둬도 작성하기에도 쉽고 읽기도 쉬운 테스트 코드를 작성하는데 많은 도움을 얻을 수 있다고 생각한다.

    JUnit (사실은 JUnit5 중점)

    JUnit은 독립된 단위테스트(Isolated Unit Test)를 지원해주는 프레임워크이다. JUnit은 버전에 따라 발전해왔으며, 현재는 JUnit5가 일반적으로 사용되고 있다.

    JUnit5은 세 개의 하위 프로젝트의 다양한 모듈로 구성되어 있는데 JUnit5의 구성을 다음의 표현식으로 설명하기도 한다.

     

    JUnit5 = Junit Platform + JUnit Jupiter + JUnit Vintage

     

    여기서 JUnit Jupiter의 프로그래밍 모델이 테스트 코드 관련 라이브러리를 포함하는 등 실질적인 테스트 코드 작성과 관련이 있기 때문에 올바른 테스트 코드 작성 하는데 JUnit Jupiter를 이해하는 것이 중요하다. 가령 JUnit Jupiter는 assertion이나 테스트 애노테이션 등을 제공하는org.junit.jupiter 라이브러리를 제공한다.

    JUnit Platform JVM에서 테스팅 프레임워크를 시작하기 위한 기반 역할을 한다.
    - 해당 플랫폼 위에서 동작하는 테스팅 프레임워크를 개발하기 위해 TestEngine API를 정의한다.
    - 하나 이상의 테스트 엔진을 사용하여 커스텀 test suite를 실행하는 JUnit Platform Suite엔진을 제공한다.
    - IntelliJ IDEA, Eclipse와 같은 유명 IDE들이나 빌드도구(e.g. Gradle, Maven..)에 대해 최고수준의 지원이 되고 있다.
    JUnit Jupiter 실제 테스트를 작성하는데 필요한 프로그래밍 모델(e.g. jupiter 내장 라이브러리)과 확장 모델의 조합이다.
    - 플랫폼 위에서 Jupiter가 쓰인 테스트를 실행 하는데 필요한 TestEngine을 제공한다.
    JUnit Vintage Junit3나 Junit4를 동작하는데 필요한 TestEngine을 제공한다.

     

    공식 문서에 따르면 JUnit5의 Jupiter에서는 테스트 작성과 관련하여 메서드와 클래스를 분류하고 다음과 같은 정의를 내리고 있다.

    라이프사이클 메서드 @BeforeEach, @AfterEach, @BeforeAll, @AfterAll 가 붙은 메서드
    테스트 메서드 @Test, @RepeatedTest, @TestFactory, @ParameterizedTest, @TestTemplate이 붙은 메서드
    테스트 클래스 테스트 메서드를 가지는 클래스 (@Nested 클래스, static으로 된 멤버 클래스 포함)

     

    이들( 라이프사이클 메서드, 테스트 메서드, 테스트 클래스)은 반드시 public일 필요는 없지만 private이면 안 된다.

    JUnit5 vs 4

    한편, JUnit5를 구성하는 JUnit Jupiter는 JUnit4의 RulesRunners와 같은 일부 기능을 자체적으로 지원하지 않는다. 그렇다면 JUnit4를 사용하던 테스트코드가 있고 JUnit5를 사용하려고 할 때 라이브러리를 사용하는 테스트 코드의 모든 부분을 수정을 해야할까? 그렇지 않다!

    JUnit의 JUnit Vintage 테스트 엔진이 JUnit Platform 인프라를 사용하여 5버전 아래의 구버전을 JUnit5로 마이그레이션을 부드럽게 이행해준다. 특히나 JUnit5에서 사용하는 애노테이션이나 클래스는 JUnit Jupiter가 제공하는 패키지 안에 있기 때문에 JUnit4의 테스트들과 충돌하지 않는다. 다만, junit-vintage-engine의 아티팩트는 test 런타임 경로에 있어야 JUnit4를 같이 사용하는데 문제가 없다. 경로가 일치해야 JUnit Platform 런처가 자동으로 구버전의 테스트들을 선택하기 때문이다.

    JUnit5가 Runners나 Rules를 자체 지원하지 않듯이 이전 버전에서 쓰이던 애너테이션들 중 일부를 지원하지 않는다. 가령, JUnit4에서 @Test를 실행하기 전에 특정 메서드를 매번 실행하기 위한 애노테이션으로 @Before를 사용했지만, JUnit5에서는 이를 지원하지 않고 @BeforeEach로 대체한다. 완전히 구버전을 fade-out시키고 JUnit5를 사용하고자 한다면 공식문서를 참고하여 기존 코드의 일부 애너테이션들을 수정해야 한다. 아래는 4,5 두 버전에서 같은 목적으로 사용되지만 다르게 표현되는 애너테이션들의 예시이다.

    JUnit4 JUnit5
    @Before / @After @BeforeEach / @AfterAll
    @BeforeClass / @AfterClass @BeforeAll / @AfterAll
    @Ignore @Disabled
    @RunWith @ExtendWith
    @Category @Tag

    JUnit의 테스트 코드 어떻게 동작할까? (Test 인스턴스 생명주기)

    기본적으로 JUnit은 테스트 메서드마다 독립된 단위테스트 환경을 지원하기 위해, 각 테스트 메서드를 실행하기 전에 해당하는 테스트 클래스의 새 인스턴스를 생성한다. (심지어 @Disabled가 붙은 테스트 메서드도 테스트 인스턴스가 생성된다.) 즉, 테스트 메서드마다 새로운 테스트 클래스가 생기고, 이로써 가변적인 테스트 인스턴스 상태로 인해 발생할 수 있는 부작용(side-effeect)이 발생하는 것을 방지한다.

    그런데 테스트 클래스의 모든 테스트 메서드들이 매번 새로운 인스턴스를 생성하지 않고 하나의 테스트 클래스 인스턴스만 생성하여 공유하여 테스트할 수도 있다. 테스트 클래스에 @TestInstance(LifeCycle.PER_CLASS)를 붙여주면 인스턴스를 공유하게 할수 있다. 이 때 매번 초기화된 상태일 필요가 있는 것은 아래 코드처럼 @BeforeEach, @AfterEach 가 붙은 라이프사이클 메서드 안에서 처리해주면 된다.

    // 테스트 메서드마다 매번 새로 만들지 않고 class를 공유할 때
    @TestInstance(LifeCycle.PER_CLASS)
    class UserServiceTest {
    
        private UserRepository userRepository;
        ...
    
        @BeforeEach //공유하지 않고 매번 생성
        void setUp() {
           userRepository = mock(UserRepository.class);
           ...
        }
    
        @Test // 공유
        void getUser_thorwsException_whenUserNoExists() {
           ... 
        }
    
    }

    이렇게 테스트 클래스 인스턴스를 하나만 생성하는 방법은 non-static 메서드, 인터페이스의 디폴트 메서드, 심지어 @Nested 클래스 내부에서도 메서드에 @BeforeAll, @AfterAll를 붙일 수 있게한다는 장점을 가지고 있다.

    Assertions의 작성 (JUnit5 vs Hamcrest vs AssertJ)

    단위 테스트 코드를 작성할 때 특정 함수가 호출되었는지, 결과 값이 예상한 값과 같은지 확인하는 등의 방법으로 특정 기능을 테스트하는 코드를 작성할 수 있다. 그 중에서도 테스트 대상이 테스트 기능을 실행한 결과 값을 기대 값과 비교 확인하는 방법이 가장 쉽고 많이 쓰인다.

    이렇게 값을 확인할 때 JUnit5에서는 org.junit.jupiter.api.Assertions 를 제공하여 비교하는 테스트 기능을 지원한다. 참고로 이 Assertions클래스 안에 있는 모든 확인 메서드들은 static 메서드이다.

    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    class AssertionsTests {
    
        ...
    
        @Test
        void test1_AssertionsTests() {
            ...
            User user = userRepository.findById("dki23i");
            assertEquals(2, user.getAge()));
            assertTrue(user.isCitizen());
        }

    Third-party Assertion 라이브러리

    Hamcrest, AssertJ, Truth는 JUnit5가 제공하지 않는 써드파티 라이브러리이다. (Truth는 내가 잘 안 쓴 관계로 용례에서 뺄 것이기에 위 소제목에서 뺐다.)

    JUnit4에서는 org.junit.Assert 클래스가 Hamcrest Matcher와 함께 쓸 수 있는 assertThat()를 제공했는데 JUnit5로 오면서 JUnit 내에서 자체적으로 assertThat()을 제공하지 않게 되었다.

    대신에 써드파티 라이브러리를 사용을 권장하여 matchers와 같은 추가 기능을 활용한 표현력이 풍부하고 가독성이 좋은 테스트 코드를 작성하도록 하고 있다.

    // Hamcrest 방법
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.hamcrest.Matchers.*;
    
    import org.junit.jupiter.api.Test;
    
    class HamcrestAssertionsDemo {
    
        private String domain;
    
        @BeforeEach
        void setUp() {
            domain = "naver.com";
        }
    
    
        @Test
        void junit_jupiter_assertions() {
            String email = "test@naver.com";
            assertThat(email, containsString(domain));
        }
    }
    // AssertJ 방법
    import static org.assertj.core.api.Assertions.assertThat;
    
    import org.junit.jupiter.api.Test;
    
    class AssertJAssertionsDemo {
    
        private String domain;
    
        @BeforeEach
        void setUp() {
            domain = "naver.com";
        }
    
    
        @Test
        void junit_jupiter_assertions() {
            String email = "test@naver.com";
            assertThat(email).contains(domain);
        }
    }

     

    hamcrest와 AssertJ를 간단하게 비교해보자면 이렇다.

     

    편의성 : AssertJ > Hamcrest
    최신성 : AssertJ > Hamcrest

     

    AssertJ는 메서드 체인 형태로 코드를 자동 완성하여 테스트 코드를 작성할 때 검증 대상에 따라 지원되는 메소드들을 바로바로 쉽게 파악할 수 있는 반면, Hamcrest는 딱 맞는 매처를 찾기 위해 일일이 api 문서를 봐야한다.

    또한 AssertJ는 2016년 1월 부터 현재까지 꾸준이 버전 관리고 되고 있는 반면, Hamcrest는 2012년 이후로 버전 업그레이드가 되지 않고 있다.

     

    결론적으로 matchers를 사용한 assertThat()를 사용하고자 한다면 AssertJ를 쓰는 것이 안전하고 가독성이 높다.

     

    출처

    https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle

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

    https://yozm.wishket.com/magazine/detail/1748/

    https://medium.com/javarevisited/execution-order-of-junit-annotations-cda673cda470

    https://medium.com/@Matiasilva/basics-of-junit-testing-b12c680aa9e9

    https://medium.com/@alexeynovikov_89393/junit5-top-7-good-to-know-things-772f6d759e37

    https://medium.com/@sridharnarayanmkr107/spring-boot-rest-api-junit-testing-a86d8bc24c25

    https://annaduldiier.medium.com/assertj-vs-junit-483b7d6dc997

    https://expert0226.tistory.com/335