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를 추천드립니다.