From 6498237c43878a25014d925689e0d234df89932e Mon Sep 17 00:00:00 2001 From: HoYeon Lee Date: Thu, 11 May 2023 20:53:41 +0900 Subject: [PATCH 1/5] =?UTF-8?q?test:=20RestDocs=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adapter-in/web/build.gradle.kts | 37 +++++++++++++- .../main/kotlin/yapp/rating/TestController.kt | 20 ++++++++ .../yapp/rating/boardgame/WebApplication.kt | 2 +- .../test/kotlin/yapp/rating/ControllerTest.kt | 51 +++++++++++++++++++ .../kotlin/yapp/rating/TestControllerTest.kt | 19 +++++++ adapter-out/rdb/build.gradle.kts | 4 +- build.gradle.kts | 23 +++++---- 7 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 adapter-in/web/src/main/kotlin/yapp/rating/TestController.kt create mode 100644 adapter-in/web/src/test/kotlin/yapp/rating/ControllerTest.kt create mode 100644 adapter-in/web/src/test/kotlin/yapp/rating/TestControllerTest.kt diff --git a/adapter-in/web/build.gradle.kts b/adapter-in/web/build.gradle.kts index 87f2fbf0..e1b0fc5e 100644 --- a/adapter-in/web/build.gradle.kts +++ b/adapter-in/web/build.gradle.kts @@ -1,16 +1,51 @@ +import groovy.lang.Closure +import io.swagger.v3.oas.models.servers.Server import org.springframework.boot.gradle.tasks.bundling.BootJar +plugins { + id("com.epages.restdocs-api-spec") version "0.18.0" +} + dependencies { val springVersion by properties + implementation(project(":support:yaml")) implementation(project(":core")) implementation("org.springframework.boot:spring-boot-starter-web:$springVersion") testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion") + testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc:3.0.0") + testImplementation("com.epages:restdocs-api-spec-mockmvc:0.18.0") } tasks { withType { enabled = false } - withType { enabled = true } + withType { + enabled = true +// mainClass.set("yapp.rating.boardgame.WebApplicationKt") + } +} + +tasks { + openapi3 { + setServers( + listOf( + toServer("http://localhost:8080"), + ) + ) + title = "My API" + description = "My API description" +// tagDescriptionsPropertiesFile = "src/docs/tag-descriptions.yaml" + version = "0.1.0" + format = "yaml" + } + + postman { + title = "My API" + version = "0.1.0" + baseUrl = "https://localhost:8080" + } } + +fun toServer(url: String): Closure = closureOf { this.url = url } as Closure diff --git a/adapter-in/web/src/main/kotlin/yapp/rating/TestController.kt b/adapter-in/web/src/main/kotlin/yapp/rating/TestController.kt new file mode 100644 index 00000000..0fdf3429 --- /dev/null +++ b/adapter-in/web/src/main/kotlin/yapp/rating/TestController.kt @@ -0,0 +1,20 @@ +package yapp.rating + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +// TODO: Delete 테스트 코드 전용 클래스 +@RestController +@RequestMapping("/v1/test") +class TestController { + + @GetMapping + fun testGet(): TestResponse { + return TestResponse("Good!") + } + + data class TestResponse( + val value: String + ) +} diff --git a/adapter-in/web/src/main/kotlin/yapp/rating/boardgame/WebApplication.kt b/adapter-in/web/src/main/kotlin/yapp/rating/boardgame/WebApplication.kt index 95f21117..def7abf6 100644 --- a/adapter-in/web/src/main/kotlin/yapp/rating/boardgame/WebApplication.kt +++ b/adapter-in/web/src/main/kotlin/yapp/rating/boardgame/WebApplication.kt @@ -3,7 +3,7 @@ package yapp.rating.boardgame import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication -@SpringBootApplication +@SpringBootApplication(scanBasePackages = ["yapp.rating"]) open class WebApplication fun main(args: Array) { diff --git a/adapter-in/web/src/test/kotlin/yapp/rating/ControllerTest.kt b/adapter-in/web/src/test/kotlin/yapp/rating/ControllerTest.kt new file mode 100644 index 00000000..6365eb79 --- /dev/null +++ b/adapter-in/web/src/test/kotlin/yapp/rating/ControllerTest.kt @@ -0,0 +1,51 @@ +package yapp.rating + +import com.fasterxml.jackson.databind.ObjectMapper +import io.kotest.core.spec.style.FunSpec +import org.springframework.restdocs.ManualRestDocumentation +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation +import org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder + +// @ExtendWith(RestDocumentationExtension::class) +abstract class ControllerTest : FunSpec() { + protected abstract val controller: Any + protected lateinit var mockMvc: MockMvc + private set + + private val restDocumentation = ManualRestDocumentation() + + init { + beforeSpec { + setUpMockMvc() + } + beforeEach { + restDocumentation.beforeTest(javaClass, it.name.testName) + } + afterEach { + restDocumentation.afterTest() + } + } + + private fun setUpMockMvc() { + mockMvc = MockMvcBuilders.standaloneSetup(controller) + .apply( + MockMvcRestDocumentation.documentationConfiguration(restDocumentation) + .uris().withScheme("http").withHost("localhost").and() + .operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint()) + ) +// .setControllerAdvice(ExceptionHandler()) +// .setCustomArgumentResolvers() +// .setMessageConverters() + .build() + } + + companion object { + @JvmStatic + protected val objectMapper = ObjectMapper() + } +} diff --git a/adapter-in/web/src/test/kotlin/yapp/rating/TestControllerTest.kt b/adapter-in/web/src/test/kotlin/yapp/rating/TestControllerTest.kt new file mode 100644 index 00000000..c30908e1 --- /dev/null +++ b/adapter-in/web/src/test/kotlin/yapp/rating/TestControllerTest.kt @@ -0,0 +1,19 @@ +package yapp.rating + +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class TestControllerTest : ControllerTest() { + override val controller = TestController() + + init { + test("Get Test") { + mockMvc.perform(get("/v1/test")) + .andExpect(status().isOk) + .andDo( + document("test") + ) + } + } +} diff --git a/adapter-out/rdb/build.gradle.kts b/adapter-out/rdb/build.gradle.kts index 0e11789e..0805d489 100644 --- a/adapter-out/rdb/build.gradle.kts +++ b/adapter-out/rdb/build.gradle.kts @@ -25,12 +25,10 @@ dependencies { api(project(":port-out")) implementation("org.springframework.boot:spring-boot-starter-data-jpa:$springVersion") - - // DB connect - runtimeOnly("com.h2database:h2") runtimeOnly("mysql:mysql-connector-java:8.0.33") testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion") + testRuntimeOnly("com.h2database:h2") } tasks { diff --git a/build.gradle.kts b/build.gradle.kts index cd0ed667..b9090cb4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,10 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { kotlin("jvm") version "1.7.22" kotlin("kapt") version "1.7.22" id("org.jlleitschuh.gradle.ktlint") version "10.2.0" - id("io.spring.dependency-management") version "1.1.0" - id("org.springframework.boot") } @@ -26,18 +26,21 @@ subprojects { dependencies { val kotestVersion: String by properties + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.0") + testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") } - tasks.withType { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = "17" + tasks { + withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "17" + } + } + test { + useJUnitPlatform() } - } - - tasks.withType { - useJUnitPlatform() } } From 75a5f1eef0987a4b012f6fe28ef30001371b7f32 Mon Sep 17 00:00:00 2001 From: HoYeon Lee Date: Thu, 11 May 2023 21:47:11 +0900 Subject: [PATCH 2/5] feat: swagger generate --- .gitignore | 1 + adapter-in/web/build.gradle.kts | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 153a6dba..1cd1e0eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Custom .idea +**/src/main/resources/static/swagger # Created by https://www.toptal.com/developers/gitignore/api/kotlin,java,gradle,intellij+all # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,java,gradle,intellij+all diff --git a/adapter-in/web/build.gradle.kts b/adapter-in/web/build.gradle.kts index e1b0fc5e..57a308ed 100644 --- a/adapter-in/web/build.gradle.kts +++ b/adapter-in/web/build.gradle.kts @@ -1,9 +1,11 @@ import groovy.lang.Closure import io.swagger.v3.oas.models.servers.Server +import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI import org.springframework.boot.gradle.tasks.bundling.BootJar plugins { id("com.epages.restdocs-api-spec") version "0.18.0" + id("org.hidetake.swagger.generator") version "2.19.2" } dependencies { @@ -17,17 +19,18 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion") testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc:3.0.0") testImplementation("com.epages:restdocs-api-spec-mockmvc:0.18.0") + swaggerCodegen("io.swagger.codegen.v3:swagger-codegen-cli:3.0.42") + swaggerUI("org.webjars:swagger-ui:4.18.2") } tasks { withType { enabled = false } withType { enabled = true -// mainClass.set("yapp.rating.boardgame.WebApplicationKt") + dependsOn("copySwaggerUI") } -} -tasks { + val generateSwaggerUIPrefix = "Api" openapi3 { setServers( listOf( @@ -40,12 +43,28 @@ tasks { version = "0.1.0" format = "yaml" } - postman { title = "My API" version = "0.1.0" baseUrl = "https://localhost:8080" } + swaggerSources { + create(generateSwaggerUIPrefix).apply { + setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml")) + } + } + withType { + dependsOn("openapi3") + } + register("copySwaggerUI") { + dependsOn("generateSwaggerUI$generateSwaggerUIPrefix") + + val generateSwaggerUISampleTask = + this@tasks.named("generateSwaggerUI$generateSwaggerUIPrefix").get() + + from("${generateSwaggerUISampleTask.outputDir}") + into("src/main/resources/static/swagger") + } } fun toServer(url: String): Closure = closureOf { this.url = url } as Closure From 1aefc2de89d9731633bab99ac5ab246b25dd1cd0 Mon Sep 17 00:00:00 2001 From: HoYeon Lee Date: Thu, 11 May 2023 21:47:51 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20Swagger=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=ED=95=98=EC=97=AC=20Readme=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index e832a8cb..419989ed 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,23 @@ ./gradlew addKtlintCheckGitPreCommitHook ``` +# Swagger + +```text +./gradlew copySwaggerUI +``` +위 Task를 실행하면 크게 3가지 작업을 진행한다. +1. Test 실행 +2. Controller 테스트 실행된 결과 snippet 을 저장 +3. snippet을 이용하여 Swagger 생성 + +Swagger 파일은 src/main/resources/static/swagger 아래에 저장된다. + +### References + +[RestDoc 세팅](https://techblog.woowahan.com/2597/) +[Swagger 파일 생성](https://jwkim96.tistory.com/274) + # Module ## 헥사고날 아키텍쳐 From 5c32dc647eafc8c4b270d682339ed5a3e5e0d20e Mon Sep 17 00:00:00 2001 From: HoYeon Lee Date: Sat, 13 May 2023 12:46:15 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20BooJar=20build=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adapter-in/web/build.gradle.kts | 5 ++++- build.gradle.kts | 7 +++++++ core/build.gradle.kts | 6 ++++++ domain/build.gradle.kts | 6 ++++++ port-in/build.gradle.kts | 7 +++++++ port-out/build.gradle.kts | 7 +++++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/adapter-in/web/build.gradle.kts b/adapter-in/web/build.gradle.kts index 87f2fbf0..2ee136d2 100644 --- a/adapter-in/web/build.gradle.kts +++ b/adapter-in/web/build.gradle.kts @@ -12,5 +12,8 @@ dependencies { tasks { withType { enabled = false } - withType { enabled = true } + withType { + enabled = true + mainClass.set("yapp.rating.boardgame.WebApplicationKt") + } } diff --git a/build.gradle.kts b/build.gradle.kts index cd0ed667..c884aba3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + plugins { kotlin("jvm") version "1.7.22" kotlin("kapt") version "1.7.22" @@ -13,6 +15,11 @@ repositories { google() } +tasks { + withType { enabled = true } + withType { enabled = false } +} + subprojects { apply(plugin = "kotlin") apply(plugin = "kotlin-kapt") diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1a0a0f3e..4f705844 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,3 +1,4 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar dependencies { val springVersion by properties @@ -9,3 +10,8 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion") } + +tasks { + withType { enabled = true } + withType { enabled = false } +} diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index e69de29b..10c43026 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -0,0 +1,6 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +tasks { + withType { enabled = true } + withType { enabled = false } +} diff --git a/port-in/build.gradle.kts b/port-in/build.gradle.kts index c7015d8e..5135a5ae 100644 --- a/port-in/build.gradle.kts +++ b/port-in/build.gradle.kts @@ -1,3 +1,10 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + dependencies { api(project(":domain")) } + +tasks { + withType { enabled = true } + withType { enabled = false } +} diff --git a/port-out/build.gradle.kts b/port-out/build.gradle.kts index c7015d8e..5135a5ae 100644 --- a/port-out/build.gradle.kts +++ b/port-out/build.gradle.kts @@ -1,3 +1,10 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + dependencies { api(project(":domain")) } + +tasks { + withType { enabled = true } + withType { enabled = false } +} From bc9e03e55eba7db7094fff1222f3c130c71f666e Mon Sep 17 00:00:00 2001 From: HoYeon Lee Date: Sat, 13 May 2023 13:07:55 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20CI=EC=97=90=20Gradle=20Build=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee636f2b..8baa467f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test +name: Continuous Integration on: pull_request: @@ -14,21 +14,21 @@ jobs: - name: Checkout uses: actions/checkout@v3 - # JDK Setup - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'adopt' - # Gradle Wrapper 권한 부여 - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Gradle Clean & Build + run: ./gradlew clean build + - name: Check ktlint format run: ./gradlew ktlintCheck - # Gradle Test를 실행한다 - name: Test with Gradle run: ./gradlew test