From 418cde4ff6c818d7f1aede2f75222af3fd9a6e07 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Mon, 17 Jun 2024 20:16:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Jooq=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=99=80=20TestContainer=20=EC=84=A4=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: testcontainers 의존성 추가 * feat: testcontainers를 위한 docker-compose 추가 * feat: RepoTestContainerInitializer 구현 * feat: JooqTestConfig 추가 * feat: JooqTestSpec 추가 * feat: RepoTestContainerInitializer 적용 * feat: application-test 구성 추가 * test: Jooq 사용 예시를 위한 _SampleJooqTest 구현 * refactor: 변경된 스키마에 맞추어 테스트 수정 --- api-repo/build.gradle.kts | 4 + .../api/repo/RepoTestContainerInitializer.kt | 28 +++ .../com/few/api/repo/jooq/JooqTestConfig.kt | 11 + .../com/few/api/repo/jooq/JooqTestSpec.kt | 12 ++ .../com/few/api/repo/jooq/_SampleJooqTest.kt | 194 ++++++++++++++++++ .../src/test/resources/application-test.yml | 6 + .../src/test/resources/docker-compose.yml | 14 ++ .../test/resources/mysql-init.d/00_init.sql | 13 ++ buildSrc/src/main/kotlin/DependencyVersion.kt | 1 + .../entity/V1.00.0.0__draft_table_design.sql | 11 + 10 files changed, 294 insertions(+) create mode 100644 api-repo/src/test/kotlin/com/few/api/repo/RepoTestContainerInitializer.kt create mode 100644 api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestConfig.kt create mode 100644 api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestSpec.kt create mode 100644 api-repo/src/test/kotlin/com/few/api/repo/jooq/_SampleJooqTest.kt create mode 100644 api-repo/src/test/resources/application-test.yml create mode 100644 api-repo/src/test/resources/docker-compose.yml create mode 100644 api-repo/src/test/resources/mysql-init.d/00_init.sql create mode 100644 data/db/migration/entity/V1.00.0.0__draft_table_design.sql diff --git a/api-repo/build.gradle.kts b/api-repo/build.gradle.kts index b978aab7d..a975c11e0 100644 --- a/api-repo/build.gradle.kts +++ b/api-repo/build.gradle.kts @@ -36,6 +36,10 @@ dependencies { implementation("org.jooq:jooq-meta:${DependencyVersion.JOOQ}") implementation("org.jooq:jooq-codegen:${DependencyVersion.JOOQ}") jooqCodegen("org.jooq:jooq-meta-extensions:${DependencyVersion.JOOQ}") + + /** test container */ + implementation(platform("org.testcontainers:testcontainers-bom:${DependencyVersion.TEST_CONTAINER}")) + testImplementation("org.testcontainers:mysql") } /** copy data migration */ diff --git a/api-repo/src/test/kotlin/com/few/api/repo/RepoTestContainerInitializer.kt b/api-repo/src/test/kotlin/com/few/api/repo/RepoTestContainerInitializer.kt new file mode 100644 index 000000000..724f2011d --- /dev/null +++ b/api-repo/src/test/kotlin/com/few/api/repo/RepoTestContainerInitializer.kt @@ -0,0 +1,28 @@ +package com.few.api.repo + +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.testcontainers.containers.DockerComposeContainer +import java.io.File + +class RepoTestContainerInitializer : ApplicationContextInitializer { + private val log: org.slf4j.Logger = + org.slf4j.LoggerFactory.getLogger(RepoTestContainerInitializer::class.java) + + companion object { + private const val MYSQL = "mysql" + private const val MYSQL_PORT = 3306 + + private val dockerCompose = + DockerComposeContainer(File("src/test/resources/docker-compose.yml")) + .withExposedService(MYSQL, MYSQL_PORT) + } + + override fun initialize(applicationContext: ConfigurableApplicationContext) { + log.debug("===== set up test containers =====") + + dockerCompose.start() + + log.debug("===== success set up test containers =====") + } +} diff --git a/api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestConfig.kt b/api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestConfig.kt new file mode 100644 index 000000000..5e764a09f --- /dev/null +++ b/api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestConfig.kt @@ -0,0 +1,11 @@ +package com.few.api.repo.jooq + +import com.few.api.repo.config.ApiRepoConfig +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.ComponentScan + +@TestConfiguration +@EnableAutoConfiguration +@ComponentScan(basePackages = [ApiRepoConfig.BASE_PACKAGE]) +class JooqTestConfig diff --git a/api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestSpec.kt b/api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestSpec.kt new file mode 100644 index 000000000..9a0de1873 --- /dev/null +++ b/api-repo/src/test/kotlin/com/few/api/repo/jooq/JooqTestSpec.kt @@ -0,0 +1,12 @@ +package com.few.api.repo.jooq + +import com.few.api.repo.RepoTestContainerInitializer +import com.few.api.repo.config.ApiRepoConfig +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration + +@ActiveProfiles("new", "test", "api-repo-local") +@SpringBootTest(classes = [ApiRepoConfig::class]) +@ContextConfiguration(initializers = [RepoTestContainerInitializer::class]) +abstract class JooqTestSpec diff --git a/api-repo/src/test/kotlin/com/few/api/repo/jooq/_SampleJooqTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/jooq/_SampleJooqTest.kt new file mode 100644 index 000000000..ba71ee587 --- /dev/null +++ b/api-repo/src/test/kotlin/com/few/api/repo/jooq/_SampleJooqTest.kt @@ -0,0 +1,194 @@ +package com.few.api.repo.jooq + +import jooq.jooq_dsl.tables.Member +import org.jooq.DSLContext +import org.jooq.JSON +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.dao.DuplicateKeyException +import org.springframework.test.annotation.Rollback +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +class _SampleJooqTest : JooqTestSpec() { + + private val log: org.slf4j.Logger = LoggerFactory.getLogger(_SampleJooqTest::class.java) + + @Autowired + private lateinit var dslContext: DSLContext + + companion object { + const val EMAIL = "test1@gmail.com" + const val TYPECD: Byte = 1 + } + + @BeforeEach + fun setUp() { + log.debug("===== start setUp =====") + dslContext.deleteFrom(Member.MEMBER).execute() + dslContext.insertInto(Member.MEMBER) + .set(Member.MEMBER.EMAIL, EMAIL) + .set(Member.MEMBER.TYPE_CD, TYPECD) + .execute() + log.debug("===== finish setUp =====") + } + + @Test + @Transactional + fun `새로운 정보를 저장합니다`() { + // given + val email = "test2@gmail.com" + val typeCd: Byte = 1 + + // when + val result = dslContext.insertInto(Member.MEMBER) + .set(Member.MEMBER.EMAIL, email) + .set(Member.MEMBER.TYPE_CD, typeCd) + .execute() + + // then + assert(result > 0) + } + + @Test + @Transactional + fun `이메일이 중복되는 경우 저장에 실패합니다`() { + // when & then + assertThrows { + dslContext.insertInto(Member.MEMBER) + .set(Member.MEMBER.EMAIL, EMAIL) + .set(Member.MEMBER.TYPE_CD, TYPECD) + .execute() + } + } + + @Test + @Transactional + fun `이메일 값을 입력하지 않은면 저장에 실패합니다`() { + // when & then + assertThrows { + dslContext.insertInto(Member.MEMBER) + .set(Member.MEMBER.TYPE_CD, TYPECD) + .execute() + } + } + + @Test + @Transactional + fun `타입 코드 값을 입력하지 않은면 저장에 실패합니다`() { + // when & then + assertThrows { + dslContext.insertInto(Member.MEMBER) + .set(Member.MEMBER.EMAIL, EMAIL) + .execute() + } + } + + @Test + fun `이메일 일치 조건을 통해 정보를 조회합니다`() { + // when + val result = dslContext.selectFrom(Member.MEMBER) + .where(Member.MEMBER.EMAIL.eq(EMAIL)) + .and(Member.MEMBER.DELETED_AT.isNull()) + .fetchOne() + + // then + assert(result != null) + assert(result!!.email == EMAIL) + assert(result.typeCd == TYPECD) + assert(result.description.equals(JSON.json("{}"))) + assert(result.createdAt != null) + assert(result.deletedAt == null) + } + + @Test + fun `이메일 불일치 조건을 통해 유저를 조회합니다`() { + // when + val result = dslContext.selectFrom(Member.MEMBER) + .where(Member.MEMBER.EMAIL.ne("test2@gmail.com")) + .and(Member.MEMBER.DELETED_AT.isNull()) + .fetch() + + // then + assert(result.isNotEmpty()) + } + + @Test + @Transactional + fun `이메일을 수정합니다`() { + // given + val newEmail = "test2@gmail.com" + + // when + val update = dslContext.update(Member.MEMBER) + .set(Member.MEMBER.EMAIL, newEmail) + .where(Member.MEMBER.EMAIL.eq(EMAIL)) + .and(Member.MEMBER.DELETED_AT.isNull()) + .execute() + + val result = dslContext.selectFrom(Member.MEMBER) + .where(Member.MEMBER.EMAIL.eq(newEmail)) + .and(Member.MEMBER.DELETED_AT.isNull()) + .fetchOne() + + // then + assert(update > 0) + assert(result != null) + assert(result!!.email == newEmail) + } + + @Test + @Transactional + fun `타입 코드를 수정합니다`() { + // given + val newTypeCd: Byte = 2 + + // when + val update = dslContext.update(Member.MEMBER) + .set(Member.MEMBER.TYPE_CD, newTypeCd) + .where(Member.MEMBER.EMAIL.eq(EMAIL)) + .and(Member.MEMBER.DELETED_AT.isNull()) + .execute() + + val result = dslContext.selectFrom(Member.MEMBER) + .where(Member.MEMBER.EMAIL.eq(EMAIL)) + .and(Member.MEMBER.DELETED_AT.isNull()) + .fetchOne() + + // then + assert(update > 0) + assert(result != null) + assert(result!!.typeCd == newTypeCd) + } + + @Test + @Rollback(false) + @Transactional + fun `소프트 삭제를 수행합니다`() { + // given + val deleteTarget = dslContext.selectFrom(Member.MEMBER) + .where(Member.MEMBER.EMAIL.eq(EMAIL)) + .and(Member.MEMBER.DELETED_AT.isNull()) + .fetchOne() + + // when + val softDelete = dslContext.update(Member.MEMBER) + .set(Member.MEMBER.DELETED_AT, LocalDateTime.now()) + .where(Member.MEMBER.EMAIL.eq(EMAIL)) + .and(Member.MEMBER.DELETED_AT.isNull()) + .execute() + + val result = dslContext.selectFrom(Member.MEMBER) + .where(Member.MEMBER.EMAIL.eq(EMAIL)) + .fetchOne() + + // then + assert(deleteTarget != null) + assert(softDelete > 0) + assert(result != null) + } +} diff --git a/api-repo/src/test/resources/application-test.yml b/api-repo/src/test/resources/application-test.yml new file mode 100644 index 000000000..180fbbd59 --- /dev/null +++ b/api-repo/src/test/resources/application-test.yml @@ -0,0 +1,6 @@ +logging: + level: + org.jooq: DEBUG + org.springframework.jdbc: DEBUG + com.few.api.repo: DEBUG + org.testcontainers: INFO \ No newline at end of file diff --git a/api-repo/src/test/resources/docker-compose.yml b/api-repo/src/test/resources/docker-compose.yml new file mode 100644 index 000000000..10cfb5ee3 --- /dev/null +++ b/api-repo/src/test/resources/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.7' + +services: + mysql: + image: mysql/mysql-server:8.0.27 + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_ROOT_HOST=% + - TZ=Asia/Seoul + command: [ "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", "--lower_case_table_names=1", "--max_connections=2048", "--wait_timeout=3600" ] + ports: + - "13306:3306" + volumes: + - ./mysql-init.d:/docker-entrypoint-initdb.d diff --git a/api-repo/src/test/resources/mysql-init.d/00_init.sql b/api-repo/src/test/resources/mysql-init.d/00_init.sql new file mode 100644 index 000000000..e14ee22c0 --- /dev/null +++ b/api-repo/src/test/resources/mysql-init.d/00_init.sql @@ -0,0 +1,13 @@ +CREATE + USER 'few-test-local'@'localhost' IDENTIFIED BY 'few-test-local'; +CREATE + USER 'few-test-local'@'%' IDENTIFIED BY 'few-test-local'; + +GRANT ALL PRIVILEGES ON *.* TO + 'few-test-local'@'localhost'; +GRANT ALL PRIVILEGES ON *.* TO + 'few-test-local'@'%'; + +CREATE + DATABASE api DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE diff --git a/buildSrc/src/main/kotlin/DependencyVersion.kt b/buildSrc/src/main/kotlin/DependencyVersion.kt index 22db90ed3..0a123dbfc 100644 --- a/buildSrc/src/main/kotlin/DependencyVersion.kt +++ b/buildSrc/src/main/kotlin/DependencyVersion.kt @@ -22,6 +22,7 @@ object DependencyVersion { const val KOTEST = "5.8.0" const val KOTEST_EXTENSION = "1.1.3" const val COROUTINE_TEST = "1.8.0" + const val TEST_CONTAINER = "1.19.8" /** docs */ const val ASCIIDOCTOR = "3.3.2" diff --git a/data/db/migration/entity/V1.00.0.0__draft_table_design.sql b/data/db/migration/entity/V1.00.0.0__draft_table_design.sql new file mode 100644 index 000000000..9b6041f9c --- /dev/null +++ b/data/db/migration/entity/V1.00.0.0__draft_table_design.sql @@ -0,0 +1,11 @@ +-- 작가 및 유저 +CREATE TABLE users +( + id BIGINT NOT NULL AUTO_INCREMENT, + email VARCHAR(255) NOT NULL, + type_cd TINYINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE (email) +); \ No newline at end of file