Junit5 with Kotlin & Spring Boot

해당 코드는 Github를 확인해주세요.

Spring boot 2.2 버전부터는 Junit5 디펜던시를 기본으로 포함하고 있습니다. Junit5 주요 테스트 어노테이션과 Spring boot에서 활용법을 정리해보았습니다.

@ValueSource

@ValueSource어노테이션을 사용하면 배열을 값을 테스트 메서드로 손쉽게 전달할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
    @ParameterizedTest
    @ValueSource(strings = ["", " "])
    internal fun `isBlank `(value: String) {
        print("value: $value ") // value:  value:
        assertThat(value.isBlank()).isTrue()
    }
    @ParameterizedTest
    @ValueSource(ints = [1, 2, 3, 4])
    internal fun `ints values`(value: Int) {
        print("value: $value ") // value: 1 value: 2 value: 3 value: 4
    }

Int, String 이 이외에도 기본형 데이터 타입을 지원하고 있습니다.

EnumSource

@EnumSource 어노테이션을 통해서 Enum을 효율적으로 테스트 할 수 있습니다.

1
2
3
4
5
6
enum class Quarter(val value: Int, val description: String) {
        Q1(1, "1분기"),
        Q2(2, "2분기"),
        Q3(3, "3분기"),
        Q4(4, "4분기")
    }

각 분기를 뜻하는 Enum을 위와 같이 정리했습니다.

1
2
3
4
5
6
    @ParameterizedTest
    @EnumSource(Quarter::class)
    internal fun `분기의  value 값은 1 ~ 4 값이다`(quarter: Quarter) {
        println(quarter.name) // quarter: Q1 quarter: Q2 quarter: Q3 quarter: Q4
        assertThat(quarter.value in 1..4).isTrue()
    }

enum에 정의된 모든 값들을 출력하는 것을 확인할 수 있습니다. @EnumSource을 사용하면 모든 enum을 iterator 하기 편리합니다.

1
2
3
4
5
6
    @ParameterizedTest
    @EnumSource(value = Quarter::class, names = ["Q1", "Q2"])
    internal fun `names을 통해서 특정 enum 값만 가져올 수 있다`(quarter: Quarter) {
        print("${quarter.name} ") // quarter: Q1 quarter: Q2
        assertThat(quarter.value in 1..2).isTrue()
    }

특정 enum을 지정해서 가져오고 싶은 경우 names = ["Q1", "Q2"]을 사용하면 됩니다.

@CsvSource

@CsvSource 어노테이션을 통해서 CSV 포멧으로 테스팅을 편리하게 진행 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
    @ParameterizedTest
    @CsvSource(
            "010-1234-1234,01012341234",
            "010-2333-2333,01023332333",
            "02-223-1232,022231232"
    )
    internal fun `전화번호는 '-'를 제거한다`(value: String, expected: String) {
        val valueReplace = value.replace("-", "")
        assertThat(valueReplace).isEqualTo(expected)
    }

, 단위로 테스트 메서드의 매개변수로 값을 넘길 수 있습니다.

@MethodSource

@MethodSource 어노테이션을 통해서 복잡한 객체를 보다 쉽게 생성하고 테스트할 수 있습니다.

1
2
3
4
5
6
data class Amount(
            val price: Int,
            val ea: Int) {
        val totalPrice: Int
            get() = price * ea
    }

가격과 수량을 입력하면 totalPrice 계산하는 단순한 객체 입니다. 해당 객체를 @MethodSource를 통해서 테스트를 진행해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @ParameterizedTest
    @MethodSource("providerAmount")
    internal fun `amount total price 테스트 `(amount: Amount, expectedTotalPrice: Int) {
        assertThat(amount.totalPrice).isEqualTo(expectedTotalPrice)
    }
    companion object {
        @JvmStatic
        fun providerAmount() = listOf(
                Arguments.of(Amount(1000, 2), 2000),
                Arguments.of(Amount(2000, 5), 10000),
                Arguments.of(Amount(4000, 5), 20000),
                Arguments.of(Amount(5000, 3), 15000)
        )
    }

@MethodSource()에 입력하는 문자열과, 값을 지정하는 static 메서드명과 일치해야 합니다. 테스트 하고자 하는 객체와, 예상되는 값을 넘겨받아 다양한 객체의 경우를 쉽게 테스트할 수 있습니다.

Spring Boot

생성자 주입

@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) 어노테이션을 통해서 테스트 코드에서도 생성자 주입이 가능해 졌습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
    @ActiveProfiles("test")
    @DataJpaTest
    internal class MemberRepositoryTest(val memberRepository: MemberRepository) {
        @Test
        internal fun `members 조회 테스트`() {
            //given
            val email = "asd@asd.com"
            val name = "name"
            //when
            val member = memberRepository.save(Member(email, name))
            //then
            assertThat(member.email).isEqualTo(email)
            assertThat(member.name).isEqualTo(name)
        }
    }

DSL 지원

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    @SpringBootTest
    @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
    @ActiveProfiles("test")
    @AutoConfigureMockMvc
    internal class MemberApiTest(
            val memberRepository: MemberRepository,
            val mockMvc: MockMvc
    ) {
        @Test
        internal fun `test`() {
            memberRepository.saveAll(listOf(
                    Member("email1@asd.com", "jin"),
                    Member("email2@asd.com", "yun"),
                    Member("email3@asd.com", "wan"),
                    Member("email4@asd.com", "kong"),
                    Member("email5@asd.com", "joo")
            ))
            mockMvc.get("/members") {
                accept = MediaType.APPLICATION_JSON
            }.andExpect {
                content { contentType(MediaType.APPLICATION_JSON) }
                jsonPath("$[0].name") { value("jin") }
                jsonPath("$[1].name") { value("yun") }
                jsonPath("$[2].name") { value("wan") }
                jsonPath("$[3].name") { value("kong") }
                jsonPath("$[4].name") { value("joo") }
            }.andDo {
                print()
            }
        }
    }

WebMvc에서도 DSL 사용을 할 수 있습니다. Web 관련 테스트 코드를 작성하기 더욱 편리해졌습니다.

AssertJ

Junit5의 관련된 내용은 아니지만 이번 Spring Boot 2.2 Release에서 AssertJ 관련된 내용이 있어 AssertJ의 사용과 간략한 팁을 정리했습니다.

AssertJ는 개인적으로 선호하는 Test Matcher입니다. static 메서드로 동작하기 때문에 자동 완성으로 Matcher 기능들을 손쉽게 사용할 수 있고, Matcher에서 지원해주는 기능도 막강합니다. AssertJ에서는 BDD 스타일의 BDDAssertion을 제공해주고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @Test
    internal fun `member save test`() {
        //given
        val email = "asd@asd.com"
        val name = "name"
        //when
        val member = memberRepository.save(Member(email, name))
        //then
        // 기존 사용법 assertThat 
        assertThat(member.email).isEqualTo(email)
        assertThat(member.name).isEqualTo(name)
        assertThat(member.createdAt).isBeforeOrEqualTo(LocalDateTime.now())
        assertThat(member.updatedAt).isBeforeOrEqualTo(LocalDateTime.now())
        // BDD 사용법
        then(member.email).isEqualTo(email)
        then(member.name).isEqualTo(name)
        then(member.createdAt).isBeforeOrEqualTo(LocalDateTime.now())
        then(member.updatedAt).isBeforeOrEqualTo(LocalDateTime.now())
    }

assertThat -> then 으로 대체되었습니다. 코드도 적어지고 더 직관적으로 되어서 좋아졌습니다.

1
2
3
4
5
6
7
    @Test
    internal fun `문장 검사`() {
        then("AssertJ is best matcher").isNotNull()
                .startsWith("AssertJ")
                .contains(" ")
                .endsWith("matcher")
    }

위와 같은 형식으로 코드를 연결해서 테스트할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    @Test
    internal fun `findByName test`() {
        //given
        memberRepository.saveAll(listOf(
                Member("email1@asd.com", "kim"),
                Member("email2@asd.com", "kim"),
                Member("email3@asd.com", "kim"),
                Member("email4@asd.com", "name"),
                Member("email5@asd.com", "name")
        ))
        //when
        val members = memberRepository.findByName("kim")
        //then
        then(members).anySatisfy {
            then(it.name).isEqualTo("kim")
        }
    }

anySatisfy 람다 표현식으로 members를 iterator 돌리면서 해당 kim과 일치하는지 편리하게 확인할 수 있습니다. 이 밖에도 다양한 것들을 제공하고 있고 계속 발전하고 있는 AssertJ를 추천드립니다.

참고


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.