ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JUnit] JUnit 핵심 (3) 의존성 주입 / 반복 테스트 / 파라미터 테스트 / 동적 테스트 / Hamcrest vs AssertJ
    DEV/JUnit 2024. 6. 26. 03:00

    JUnit5 이전에는 생성자나 메서드에 파라미터가 있는 것을 허용하지 않았고 테스트는 반드시 기본 생성자만 사용해야했다.

    JUnit5부터는 이러한 부분이 의존성 주입으로 사용 가능하게 되었다.

     

    ParameterResolver 인터페이스는 런타임에 파라미터를 동적으로 리졸브한다.

    현재 JUnit5에는 3개의 리졸버가 기본으로 내장되어있다.

    다른 파라미터 리졸버를 사용하려면 @ExtendWith로 적절한 extension을 적용하여 파라미터 리졸버를 명시해야한다.

     

    - TestInfoParameterResolver

    : 현재 실행중인 테스트나 컨테이너에 관한 정보를 제공하기 위해 사용되는 TestInfo 객체를 파라미터로 사용 가능

    • 디스플레이 네임, 테스트 클래스, 테스트 메서드, 관련 태그 정보 등
    import org.junit.jupiter.api.TestInfo;
    
    public class TestInfoTest {
        
        TestInfoTest(TestInfo testInfo) {
            assertEquals("TestInfoTest", testInfo.getDisplayName());
        }
    }

     

    - TestReporterParameterResolver

    : 현재 실행되는 테스트에 추가적인 정보를 제공하기위해 테스트 리포트를 만들 때 사용되는 TestRepoter 객체를 파라미터로 사용 가능

    • 함수형 인터페이스이므로 람다식이나 메서드 참조로 사용 가능
    • 1개의 추상 메서드 publishEntry와 publishEntry를 오버로딩한 여러개의 디폴트 메서드를 가진다.
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.TestReporter;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class TestReporterTest {
    
        @Test
        void testReportSingleValue(TestReporter reporter) {
            reporter.publishEntry("Single value");
        }
    
        @Test
        void testReportKeyValuePair(TestReporter reporter) {
            reporter.publishEntry("Key", "Value");
        }
    
        @Test
        void testReportMultipleKeyValuePairs(TestReporter reporter) {
            Map<String, String> values = new HashMap<>();
            values.put("user", "John");
            values.put("password", "secret");
            reporter.publishEntry(values);
        }
    
    
    }

     

    - RepetitionInfoParameterResolver

    : @RepeatedTest, @BeforeEach, @AfterEach 애노테이션이 달린 메서드의 파라미터가 RepetitionInfo 타입일때 RepetitionInfo 인스턴스를 리졸브하는 역할. RepetitionInfo는 반복 테스트에 대한 현재 반복 인덱스와 총 반복 횟숭에 대한 정보를 가진다.

     

    - 반복 테스트 : @RepeatedTest 애노테이션을 사용하여 반복 횟수를 지정하고 해당 횟수만큼 테스트를 반복한다.

     

    지원하는 플레이스 홀더

    • {displayName} : @RepeatedTest 애노테이션이 붙은 메서드의 디스플레이 네임
    • {currentRepetition} : 현재 반복 인덱스
    • {totalRepetitions} : 총 반복 횟수
    import com.study.junit.ch01.Calculator;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.RepeatedTest;
    import org.junit.jupiter.api.RepetitionInfo;
    import org.junit.jupiter.api.TestReporter;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    import java.util.ArrayList;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    public class RepeatedTestsTest {
    
        private static Set<Integer> integerSet = new HashSet<>();
        private static List<Integer> integerList = new ArrayList<>();
    
        @RepeatedTest(value = 5, name = "{displayName} - repetition {currentRepetition}/{totalRepetitions}")
        @DisplayName("Test add operation")
        void addNumber() {
            Calculator calculator = new Calculator();
            assertEquals(2, calculator.add(1,1), "1+1 should equal 2");
        }
    
        @RepeatedTest(value = 5,
        name = "the list contains {currentRepetition} elements(s), the set contains 1 element")
        void testAddingToCollections(TestReporter reporter, RepetitionInfo repetitionInfo) {
            integerSet.add(1);
            integerList.add(repetitionInfo.getCurrentRepetition());
    
            reporter.publishEntry("Repetition number", String.valueOf(repetitionInfo.getCurrentRepetition()));
    
            assertEquals(1, integerSet.size());
            assertEquals(repetitionInfo.getCurrentRepetition(), integerList.size());
        }
    }

     

    테스트 콘솔

    이와 같이 반복 횟수 및 인덱스를 확인 할 수 있다.

     

    - 파라미터를 사용한 테스트 : @ParameterizedTest 애노테이션을 사용하여 하나의 테스트를 다양한 입력값(파라미터)을 가지고 여러번 실행할 수 있다.

    • @ValueSource : 문자열 배열을 입력값으로 지정
    • @EnumSource : 열거형을 입력값으로 지정
    • @CsvSource : CSV 형식으로 입력값 지정
    • @CsvFileSource : CSV 파일의 소스를 입력값으로 지정

    @ValueSource 예제

    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.ValueSource;
    
    public class ParameterizedTestWithValueSourceTest {
        private WordCounter wordCounter = new WordCounter();
    
        @ParameterizedTest
        @ValueSource(strings = {"Check three parameters", "JUnit in Action"})
        void testWordsInSentence(String sentence) {
            Assertions.assertEquals(3, wordCounter.countWords(sentence));
        }
    }

     

    wordCounter의 countWords메서드는 String 문자열을 split한 배열의 길이를 return 한다.

    ValueSource의 Strings 배열이 두 개이므로 테스트는 두 번 실행된다.

     

    @CsvSource 예제

    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.CsvSource;
    
    public class ParameterizedWithCsvSourceTest {
        private WordCounter wordCounter = new WordCounter();
    
        @ParameterizedTest
        @CsvSource({"2, Unit testing", "3, JUnit int Action", "4, Write solid Java code"})
        void testWordsInSentence(int expected, String sentence) {
            Assertions.assertEquals(expected, wordCounter.countWords(sentence));
        }
    }

     

    각 CSV 행 구문이 분석되어 첫번째 값은 expected에 할당되고, 두번째값은 sentence에 할당된다.

     

    나머지 예제는 깃헙 소스로 확인

    https://github.com/devkunst/junit-in-action-third-edition-kr/tree/master/ch02-core/src/test/java/com/manning/junitbook/ch02/parametrized

     

     

    - 동적 테스트 : 런타임에 테스트를 생성. 개발자가 팩터리 메서드를 작성하면 프레임워크가 런타임에 실행할 테스트를 생성한다. 팩터리 메서드는 @TestFactory 애노테이션을 달면 된다.

     

    @TestFactory 메서드가 반환할 수 있는 대상

    • DynamicNode(추상클래스이며 DynamicContainer나 DynamicTest가 DynamicNode를 상속하였고 인스턴스화가 가능한 구체 클래스이다.)
    • DynamicNode 객체의 배열
    • DynamicNode 객체의 스트림
    • DynamicNode 객체의 컬렉션
    • DynamicNode 객체의 Iterable
    • DynamicNode 객체의 Iterator

    동적 테스트는 @Test 가 달린 보통의 테스트와 다른 생애주기를 가진다.

    @BeforEach, @AfterEach 애노테이션이 달린 메서드는 @TestFactory 메서드 전체에 대해 실행될 뿐 개별 테스트(dynamic test) 각각에 대해서는 실행되지 않는다. 팩터리 메서드 외에 개별 동적 테스트에 대한 생애주기 콜백은 없다.

    @BeforeAll, @AfterAll이 동작하는 방식도 동일하다.

     

    import org.junit.jupiter.api.*;
    
    import java.util.Iterator;
    
    import static java.util.Arrays.asList;
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    import static org.junit.jupiter.api.DynamicTest.dynamicTest;
    
    public class DynamicTestsTest {
    
        private PositiveNumberPredicate predicate = new PositiveNumberPredicate();
    
        @BeforeAll
        static void setUpClass() {
            System.out.println("@BeforeAll method");
        }
    
        @AfterAll
        static void tearDownClass() {
            System.out.println("@AfterAll method");
        }
    
        @BeforeEach
        void setUp() {
            System.out.println("@BeforeEach method");
        }
    
        @AfterEach
        void tearDown() {
            System.out.println("@AfterEach method");
        }
    
        @TestFactory
        Iterator<DynamicTest> positiveNumberPredicateTestCases() {
            return asList(
                    dynamicTest("negative number",
                            () -> assertFalse(predicate.check(-1))),
                    dynamicTest("zero",
                            () -> assertFalse(predicate.check(0))),
                    dynamicTest("positive number",
                            () -> assertTrue(predicate.check(1)))
            ).iterator();
        }
    }

     

    테스트 콘솔

    동적 개별 테스트 각각 실행됐지만, 생애주기 메서드는 이와 관련없이 @TestFactory 메서드에 대해서만 한번씩 실행됐다.

     

     

    - Hamcrest 매처, Hamcrest 라이브러리

    : 간명한 매치 규칙을 선언하는데 도움이 되는 라이브러리로 단위 테스트에 유용하게 쓰인다.

     

    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.hamcrest.Matchers.*;
    import static org.junit.jupiter.api.Assertions.*;
    
    
    public class HamcrestListTest {
    
        private List<String> values;
    
        @BeforeEach
        public void setUp() {
            values = new ArrayList<>();
            values.add("John");
            values.add("Michael");
            values.add("Edwin");
        }
    
        @Test
        @DisplayName("Hamcrest를 사용하지 않은 테스트")
        public void testWithoutHamcrest() {
            assertEquals(3, values.size());
            assertTrue(values.contains("Oliver")
                        || values.contains("Jack")
                        || values.contains("Harry"));
        }
    
        @Test
        @DisplayName("Hamcrest를 사용해서 자세한 실패 정보를 나타내는 테스트")
        public void testListWithHamcrest() {
            assertThat(values, hasSize(3));
            assertThat(values, hasItem(anyOf(equalTo("Oliver"),
                    equalTo("Jack"), equalTo("Harry"))));
        }
    }

     

    Hamcrest 사용하지 않은 테스트 결과
    Hamcrest를 사용한 테스트 결과

     

    이와 같이 햄크레스트 라이브러리를 사용하면 단언문이 실패했을 때 실패한 내용에 대해 기본 단언문보다 친절하게 설명해준다. 그리고 매처 메서드를 사용할 때 중첩이 가능하단점과 가독성이 좋다는 장점이 있다고 한다.

     

    그러나 같이 스터디하는분께서 AssertJ가 훨씬 가독성이 좋고 사용하기 편하다고하여 직접 테스트 해봤다.

     

    import org.assertj.core.api.Assertions;
    ...
    
        @Test
        @DisplayName("AssertJ 사용 테스트")
        public void testWithAssertJ() {
            Assertions.assertThat(values).containsAnyOf("Oliver", "Jack", "Harry");
        }

    AssertJ를 사용한 테스트 결과

     

    이와 같이 더욱더 코드를 간결하게 줄일 수 있고 실패 결과 또한 친절하게 설명해준다 !

    그래서 나는 Hamcrest 보단 AssertJ를 사용하게 될 것 같으므로 중간에 AssertJ에 대해서도 포스팅 해야할 것같다.

     

     

    댓글

Designed by Tistory.