diff --git a/README.md b/README.md index d6cf3bd..eba0d25 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,10 @@ git clone https://gitea.pricelees.me/pricelees/kopring-validation.git // 테스트 실행 후 콘솔 출력 결과 확인 -src.test/kotlin/com/sangdol/validation/ValidationTest.kt +src/test/kotlin/com/sangdol/validation/DemoControllerTest + +// Kotest 버전 +src/test/kotlin/com/sangdol/validation/DemoControllerKTest ``` `src/main/kotlin/com/sangdol/validation/DemoDTO.kt` 를 Decompile 하여 상황에 따라 어노테이션이 어떻게 붙는지 확인할 수 있습니다. - 같은 @NotNull 이라도 jakarta / jetbrains 패키지인지에 따라 처리되는 방법이 다르며, 자세한 내용은 위 글에서 확인할 수 있습니다. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 2b67d1a..b261641 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,9 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("io.github.oshai:kotlin-logging-jvm:7.0.3") + testImplementation("io.mockk:mockk:1.14.4") + testImplementation("io.kotest:kotest-runner-junit5:5.9.1") + testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/src/test/kotlin/com/sangdol/validation/DemoControllerKTest.kt b/src/test/kotlin/com/sangdol/validation/DemoControllerKTest.kt new file mode 100644 index 0000000..5e99014 --- /dev/null +++ b/src/test/kotlin/com/sangdol/validation/DemoControllerKTest.kt @@ -0,0 +1,90 @@ +package com.sangdol.validation + +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.BehaviorSpec +import org.hamcrest.CoreMatchers.containsString +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import kotlin.random.Random + +@WebMvcTest(controllers = [DemoController::class]) +@DisplayName("타입별 Bean Validation 테스트") +class DemoControllerKTest( + @Autowired private val mockMvc: MockMvc, + @MockitoBean private val demoService: DemoService +) : BehaviorSpec() { + + init { + Given("Wrapper 타입에서는") { + val endpoint = "/wrapper" + val value = "value" + + When("값이 입력되지 않으면") { + val expectedStatus = HttpStatus.BAD_REQUEST.value() + val expectedError = "HttpMessageNotReadableException" + + Then("HttpMessageNotReadableException이 발생한다.") { + val body = "{\"withAnnotation\": \"$value\"}" + runException(endpoint, body, expectedStatus, expectedError) + } + + Then("@NotNull이 적용되지 않고 HttpMessageNotReadableException이 발생한다.") { + val body = "{\"withoutAnnotation\": \"$value\"}" + runException(endpoint, body, expectedStatus, expectedError) + } + } + } + + Given("Primitive 타입에서는") { + val endpoint = "/primitive" + val value = Random.nextInt() + + When("값을 입력하지 않아도") { + val expectedStatus = HttpStatus.OK.value() + + Then("@NotNull과 무관하게 기본값이 들어간다.") { + run(endpoint, "{\"withoutAnnotation\": \"$value\"}", expectedStatus) + } + } + } + + Given("Primitive 타입에서 @NotNull + Nullable 으로 선언한 경우에는") { + val endpoint = "/solution" + + When("값이 입력되지 않으면") { + val body = "{\"notExistField\": 10}" + val expectedStatus = HttpStatus.BAD_REQUEST.value() + val expectedError = "MethodArgumentNotValidException" + + Then("@NotNull Bean Validation이 적용된다.") { + runException(endpoint, body, expectedStatus, expectedError) + } + } + } + } + + private fun run(endpoint: String, body: String, expectedStatus: Int): ResultActions = + mockMvc.perform( + post(endpoint) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(body) + ).andExpect(status().`is`(expectedStatus)) + .andDo(print()) + + private fun runException( + endpoint: String, + body: String, + expectedStatus: Int, + expectedExceptionType: String + ): ResultActions = + run(endpoint, body, expectedStatus) + .andExpect(jsonPath("$.type", containsString(expectedExceptionType))) +} \ No newline at end of file