ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JAVA] JUnit5를 이용해 테스트 코드를 작성해보자!
    JAVA 2022. 7. 23. 00:55

     

     

    테스트 주도 개발(TDD, Test-Driven-Development)을 하기 위한 테스트코드를 작성하는 방법을 배웠다.

    JUnit4버전과 JUnit5버전 모두 배웠는데, JUnit5를 중점으로 설명하려고 한다.

     


    JUnit4 VS JUnit5

     

    우선 JUnit4버전과 5버전의 차이를 정말 간단하게만 정리하면, 사용하는 어노테이션에 차이가 있다.

     

    4버전에서의 @Before, @BeforeClass, @After, @AfterClass, @Ignore어노테이션이

    5버전에서는 @BeforeEach, @BeforeAll, @AfterEach, @AfterAll, @Disabled로 변경됐다.

     

    4버전의 이름은 처음 접했을 때 무슨 의미인지 잘 와닿지 않았는데, 바뀐 이름으로 보니 한번에 이해할 수 있어서 직관적으로 잘 바뀐것 같다.

     

    또한 5버전부터는 JUnit Platform, JUnit Jupiter, JUnit Vintage 3가지 하위 프로젝트가 제공된다.

    새 어노테이션을 이용해 단위테스트를 작성하려면 JUnit Jupiter를 사용하면 된다.

     

    테스트 메소드의 접근제한자가 public에서 default로 바뀐 것도 차이점이다.

     


    JUnit5 사용방법

     

    메이븐 기반 프로젝트를 기준으로,

    JUnit5 프레임워크를 사용하려면 pom.xml 파일의 dependency에 JUnit Jupiter를 추가해야한다.

    (gradle을 쓴다면 gradle방식으로 등록하면된다.)

     

    메이븐 리포지토리에 "JUnit Jupiter API" 이름으로 제공되고 있다.

     

    안정화된 버전 중 가장 최신 버전인 5.8.2버전을 dependency 태그에 추가했다. 

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.8.2</version>
        <scope>test</scope>
    </dependency>

     

     

    어찌보면 당연한 얘기지만, 테스트 코드는 테스트 폴더에 작성한다.

    test/java에 자바 클래스를 생성하면 된다.

     


    테스트코드 기본 골격 - 1. Test Instance, Test Method Order

     

    클래스를 생성했으면, 클래스 상단부에 테스트 인스턴스 단위와 정렬순서를 지정할 수 있다.

    import org.junit.jupiter.api.MethodOrderer;
    import org.junit.jupiter.api.TestInstance;
    import org.junit.jupiter.api.TestInstance.Lifecycle;
    import org.junit.jupiter.api.TestMethodOrder;
    
    import lombok.NoArgsConstructor;
    import lombok.extern.log4j.Log4j2;
    
    @Log4j2
    @NoArgsConstructor
    
    @TestInstance(Lifecycle.PER_CLASS)
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    public class Test {
    	;;
    } // end class

     

    참고로 테스트클래스는 디폴트 생성자가 필요하므로, 롬복의 @NoArgsConstructor를 사용했다.

    물론 생성자를 따로 작성하지 않으면 자동으로 디폴트 생성자가 만들어지지만, 이렇게 명시해주는 것이 좋다.

    로그를 찍기 위해 log4j2도 임포트했다.

     

    @TestInstance: 테스트 인스턴스가 테스트 클래스 당 한번만 생성되게 할 것인지, 메소드마다 생성되게 할 것인지 지정할 수 있다. LifeCycle enum 상수로 제공되고, PER_CLASS, PER_METHOD 두가지가 있다.

     

    @TestMethodOrder: 테스트 메소드가 실행되는 순서를 정할 때, 어떤걸 기준으로 정할 지 정할 수 있다. 

     

     제공되는 속성을 보면 메소드 이름, 디스플레이 이름, Order 어노테이션, 랜덤을 지정할 수 있다.

    여기서 OrderAnnotation으로 지정하면 테스트 메소드에 @Order어노테이션을 붙여 순서를 원하는대로 지정할 수 있다.(둘은 세트다!)

    타입으로 clazz객체를 받기 때문에 속성을 정한 후에 .class를 붙여서 clazz객체로 만들어줘야한다.

     

     


    테스트코드 기본 골격 - 2. BeforeEach, BeforeAll, AfterEach, AfterAll

     

    before ~~~, after ~~~은 사전처리, 사후처리 메소드를 명시하는 어노테이션이다.

    테스트 코드를 실행하기 전/후에 수행해야할 로직이 있을 경우 이 어노테이션을 붙인 메소드 안에 작성하면 된다.

    ~~Each는 메소드를 실행할 때마다 수행되고, ~~~All은 전체 테스트 수행 전/후 한번만 수행된다.

    이 4가지 어노테이션은 optional이다. 따로 처리해야할 로직이 없을 경우엔 작성하지 않아도 된다.

     

     

    @BeforeAll
    void beforeAll() {
        log.trace("beforeAll() invoked.");
        log.trace("\t+ 테스트 클래스 실행 전 한번만 실행됩니다.");
    } // beforeAll
    
    @BeforeEach
    void beforeEach() {
        log.trace("beforeEach() invoked.");
        log.trace("\t+ 메소드가 수행될 때마다 전에 실행됩니다.");
    } // beforeEach
    
    @AfterEach
    void afterEach() {
        log.trace("afterEach() invoked.");
        log.trace("\t+ 메소드가 수행될 때마다 후에 실행됩니다.");
    } // afterEach
    
    @AfterAll
    void afterAll() {
        log.trace("afterAll() invoked.");
        log.trace("\t+ 테스트 클래스 실행 완료 후 한번만 실행됩니다.");
    } // afterAll
    
    @Test
    void test1() {
        log.trace("test1() invoked.");
    } // test1
    
    @Test
    void test2() {
        log.trace("test2() invoked.");
    } // test2
    
    @Test
    void test3() {
        log.trace("test3() invoked.");
    } // test3

     

    각 어노테이션의 기능을 명확히 보기 위해 테스트 메소드 3개를 만들고 전체 테스트를 실행했다.

     

    ~~Each가 붙은 메소드는 각 메소드의 전/후에 실행되고, ~~All은 시작과 끝에 한번만 실행되는 것을 확인할 수 있다.

     


    테스트코드 기본 골격 - 3. 테스트 메소드에 사용할 수 있는 어노테이션

     

     

    //	@Disabled
    	@Test
    	@Order(1)
    	@DisplayName("Test Method Name")
    	@Timeout(value = 10, unit = TimeUnit.SECONDS)
    	void test1() {
    		log.trace("test1() invoked.");
    	} // test1

     

    @Disabled : 테스트할 때 해당 메소드는 제외하고 테스트한다.

    @Test : 해당 메소드가 테스트 메소드임을 명시한다. 이 어노테이션이 없으면 테스트 실행이 안됨.

    @Order : 클래스 상단부에서 정렬기준으로 OrderAnnotation을 사용했을 경우, 이 어노테이션으로 순서를 지정한다.

    이렇게 어노테이션으로 순서를 지정하면, 코드 실행흐름상 test1 다음에 test2가 수행되어야 하지만 test2가 먼저 수행되는 것을 볼 수 있다. test1은 DisplayName에 지정한 이름으로 표시되고, test3은 disabled되었다.

     

    @DisplayName : JUnit 콘솔에서 표시되는 이름을 지정할 수 있다. 공백을 허용한다.

    @Timeout : 타임아웃을 지정해 메소드 수행시간이 지정한 시간을 초과할 경우 fail이 된다. value속성에 시간을 지정하고, unit속성에 단위를 지정할 수 있다. TimeUnit enum 상수를 사용한다. 

     

    지정할 수 있는 단위는 이렇게 많다. 이정도면 모든 단위가 제공된다고 봐도 무방...

     

     

    또한 테스트메소드는 한글이름을 지원한다. 그래서 테스트 코드를 작성할 땐 굳이 영어로 어렵게 이름을 짓지 않고도 한글로 간단명료하게 이름을 지을 수 있다.

     

    메소드명과 디스플레이네임에 둘다 한글을 입력했을 때 정상적으로 작동한다.

    DisplayName에는 공백, 특수문자도 입력가능하다.

     

     


    Assertions 클래스

     

    JUnit Jupiter에는 정적메소드로 이루어진 Assertions클래스가 있다.

    이 메소드의 결과에 따라 테스트코드의 성공/실패가 결정된다.

     

     

    assert로 시작하는 엄청 많은 메소드들이 있고, 또 엄청 많이 오버로딩 되어있다.

    메소드명이 상당히 직관적이라서 이해하기는 쉽다.

    예를들어 assertTrue는 참이면 성공, 거짓이면 실패, assertEquals는 같으면 성공, 다르면 실패 이런 식으로 동작한다.

     

     

    assertNotNull을 사용한 예이다.

    기대한 값이 NotNull인데, Null이 들어오자 테스트가 실패한 것을 확인할 수 있다.

     

     

    정적 메소드는 원래 클래스명.메소드명()으로 사용해야 하는데, 이렇게 메소드명만으로 사용할 수 있게 하기 위해선 static import 구문을 추가해야 한다. 정적 메소드를 메소드명만으로 사용할 수 있게 해주는 임포트 구문이다. Assertions 클래스의 메소드는 모두 정적메소드이므로, 이렇게 static import를 한 후 메소드명으로 사용하는게 일반적이다.

     

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

    댓글

Designed by Tistory.