TDD(Test Driven Development) - JUnit 5 (2)

아래 내용은 ‘테스트 주도 개발: TDD 실천법과 도구’(https://repo.yona.io/doortts/blog/issue/1)를 보고 주요 내용을 개인적으로 정리하여 작성했습니다.

JUnit ?


Java 에서 사용되는 Testing 프레임워크이다. 단위 테스트를 할 때 사용되는 도구이며 테스트 결과와 예상값을 확인시켜주는 Assert 와 테스트 실행을 도와주는 Test Runner 등으로 구성되어 있다.

xUnit 이라고 해서 자바 외에 다른 언어에서 사용되는 테스트 도구가 있다. 예를들어 C 의 경우 CUnit C++ 의 경우 CppUnit 등이 있다.


JUnit, Hamcrest 설정


JUnit 4 버전에서는 단일 jar 만 사용되었었는데 JUnit 5 로 넘어오면서 여러개의 jar 로 분리되었다고 한다.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform : JVM 위에서 실행되는 테스트 프레임워크 이다. JUnit 과 Client Build Tool 사이에서 강력한 인터페이스를 제공한다. 또한 JUnit 플랫폼 위에서 실행되는 테스트 프레임워크를 개발하기 위한 TestEngine API 를 제공한다. 이를 사용해서 써드파티 테스트 라이브러리를 JUnit 에 직접 플러그인 할 수 있다.
  • JUnit Jupiter : @BeforeEach, @AfterEach, 등의 5버전에서 새로이 추가된 프로그래밍 모델과 확장 모델들을 포함하고 있다. JUnit 4 에서 추가되거나 변경된 Annotation 은 다음과 같다.
    • @TestFactory : 동적 테스트를 위한 테스트 팩토리 메소드를 가리킨다.
    • @DisplayName : 테스트 클래스 또는 메소드의 사용자 정의 이름을 표시한다.
    • @Nested : 해당 Annotation 이 표시된 클래스가 중첩된 비 정적인 테스트 클래스임을 나타낸다.
    • @Tag : 필터링 테스트를 위한 태그를 선언
    • @ExtendWith : 사용자 정의 확장명을 등록하는 데 사용
    • @BeforeEach : 테스트 메소드 이전에 실행되는 메소드(@Before)
    • @AfterEach : 테스트 메소드 후에 실행되는 메소드(@After)
    • @BeforeAll : 현재 클래스의 모든 메소드 이전에 실행되는 메소드(@BeforeClass)
    • @AfterAll : 현재 클래스의 모든 메소드 이후에 실행되는 메소드(@AfterClass)
    • @Disable : 테스트 클래스 또는 메소드를 테스트에서 제외 또는 비활성화 하는데 사용(@Ignore)
  • JUnit Vintage : TestEngine 에서 JUnit 3 & JUnit 4 버전의 테스트 코드를 실행해준다.

  • JUnit 5 Features :
    • JUnit Jupiter 에서 테스트 코드를 작성
    • JUnit 4 버전과 Jupiter 통합
    • 테스트 실행
    • JUnit Jupiter 를 위한 확장 모델 제공
    • JUnit Platform 실행 API, JUnit Platform Test Kit 제공

JUnit 5 의 대략적인 변경점과 특징을 알아보았다. 이제 Maven 또는 Gradle 에서 Dependency 를 추가해보자.

Maven

<dependencies>
    <!-- 
      https://mvnrepository.com/artifact/org.junit.
      jupiter/junit-jupiter 
    -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.6.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle

// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.6.2'


JUnit 5 예제


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

import java.util.logging.Logger;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

  private static final Logger log = Logger.getLogger(DemoApplicationTests.class.getName());

  // BeforeAll 은 현재 테스트 클래스 또는 전체 테스트 메소드 실행이전에 실행되야 하므로 static 으로 작성한다.
  @BeforeAll
  static void setup() {
    log.info("@BeforeAll - executes once before all test methods in this class");
  }

  @BeforeEach
  void init() {
    log.info("@BeforeEach - executes before each test method in this class");
  }
  
  // assertEquals 사용
  @Test
  @DisplayName("1 + 1 = 2")
  void addTwoNumbers ()	{
    Calculator calculator = new Calculator();
    assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2");
  }

  // 테스트 항목 이름 설정
  @DisplayName("Single test successful")
  @Test
  void testSingleSuccessTest() {
    log.info("Success");
  }

  // JUnit4 버전의 @Ignore 와 동일한 @Disabled 사용
  @Test
  @Disabled("Not implemented yet")
  void testShowSomething() {
  }

  // Java 8 이상의 버전에서는 Lambda 식을 사용할 수 있다.
  @Test
  void lambdaExpressions() {
    List<Integer> numbers = Arrays.asList(1, 2, 3);

    assertTrue(numbers
      .stream()
      .mapToInt(i -> i)
      .sum() > 5, () -> "Sum should be greater than 5");
  }

  // 위 Lambda 식을 사용하면 lazy 로딩을 이용하여 좋은 퍼포먼스를 보일 수 있다.
  // 하지만 아래처럼 사용할 경우 보다 더 정확한 위치의 오류를 확인할 수 있다.
  @Test
  void groupAssertions() {
    int[] numbers = {0, 1, 2, 3, 4};

    assertAll("numbers",
      () -> assertEquals(numbers[0], 1),
      () -> assertEquals(numbers[3], 3),
      () -> assertEquals(numbers[4], 1)
    );
  }

  @Test
  void trueAssumption() {
    // True 경우를 가정하는 테스트 문
    assumeTrue(5 > 1);
    assertEquals(5 + 2, 7);
  }

  @Test
  void falseAssumption() {
    // False 일 경우를 가정하는 테스트 문
    assumeFalse(5 < 1);
    assertEquals(5 + 2, 7);
  }

  @Test
  void assumptionThat() {
    String someString = "Just a string";
    // assumingThat 의 경우 첫번째 인자인 가정문이 맞지 않을 경우 실행되지 않는다.
    // 만약 첫번째 인자의 가정문이 맞다고 하면 뒤에 테스트 문이 실행된다.
    assumingThat(someString.equals("Just a string"), () -> assertEquals(2 + 1, 4));
  }

  // 예외에 대한 테스트
  @Test
  void shouldThrowException() {
    // 예외의 세부사항에 대한 테스트
    Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
      throw new UnsupportedOperationException("Not supported");
    });
    assertEquals(exception.getMessage(), "Not supported");
  }

  @Test
  void assertThrowsException() {
    String str = null;
    // 예외의 유형에 대한 테스트
    assertThrows(IllegalArgumentException.class, () -> {
      Integer.valueOf(str);
    });
  }

  // 아래 두 예제는 Test suite 예제인데, @SelectPackage 같은 경우에는 테스트 패키지를 테스트하는 것이고
  // @SelectClasses 의 경우에는 선택된 클래스만 테스트하는 것이다.
  // @RunWith(JUnitPlatform.class)
  // @SelectPackages("com.baeldung")
  // public class AllUnitTest {}
  // @RunWith(JUnitPlatform.class)
  // @SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class})
  // public class AllUnitTest {}

  // 아래 예제는 동적인 테스트에 관한 예제이다. 
  // 일반적으로 위에서 보았던 테스트의 경우 정해진 정적인 값을 비교하는 테스트들이었다.
  // 하지만 아래는 고정된 값이 아닌 배열을 조회하며 각각의 값을 테스트한다.
  private List<String> in = new ArrayList<>(Arrays.asList("A", "B", "C"));
  private List<String> out = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));

  @TestFactory
  public Stream<DynamicTest> translateDynamicTestsFromStream() {
    return in.stream().map(word -> DynamicTest.dynamicTest("Test translate " + word, () -> {
      int id = in.indexOf(word);
      assertEquals(out.get(id), translate(word));
    }));
  }

  private String translate(String word) {
   if ("A".equalsIgnoreCase(word)) {
      return "aaa";
    } else if ("B".equalsIgnoreCase(word)) {
      return "bbb";
    } else if ("C".equalsIgnoreCase(word)) {
      return "ccc";
    }
    return "Error";
  }

  @AfterEach
  void tearDown() {
      log.info("@AfterEach - executed after each test method.");
  }

  @AfterAll
  static void done() {
      log.info("@AfterAll - executed after all test methods.");
  }

}

위에 코드를 보면 @Test 로 표시된 메소드를 테스트한다. 테스트 항목의 이름은 “1 + 1 = 2” 로 선언하였고 Equals 를 이용해 add(1, 1) 의 결과가 2 가 맞는지 확인한다.

해당 예제는 JUnit5 공식 사이트에 있다.

간단하게 JUnit 5의 특징과 예제를 알아보았다. 다음에는 Jupiter 의 여러 Annotation 과 테스트를 좀더 유연하게 사용하도록 도와줄 Hamcrest 를 알아보자.

댓글남기기