Skip to content

Latest commit

ย 

History

History
271 lines (220 loc) ยท 9.39 KB

File metadata and controls

271 lines (220 loc) ยท 9.39 KB

Spring Boot์—์„œ TestContainers ์‚ฌ์šฉํ•˜๊ธฐ

์˜ˆ์ „์—๋Š” OS์— ์„ค์น˜ํ•˜์—ฌ ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š” ๋งŽ์€๊ฒƒ๋“ค์ด ์ด์ œ๋Š” Docker๋ฅผ ์ด์šฉํ•˜์—ฌ ์‰ฝ๊ฒŒ ์‹คํ–‰ํ•˜๊ณ  ์ข…๋ฃŒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Local ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ ํ•ด์•ผํ•˜๋Š” Database๋„ ๊ทธ์ค‘์— ํ•˜๋‚˜์ธ๋ฐ ๋ณดํ†ต ์„œ๋น„์Šค ํ˜•ํƒœ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ํ•ญ์‹œ ์‹คํ–‰๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿด๊ฒฝ์šฐ์—๋Š” ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๋„ ์‹ฌํ•˜๊ณ  ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์„œ๋น„์Šค๋“ค๋กœ ์ธํ•˜์—ฌ ์ž‘์—…ํ•˜๋Š”๋ฐ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ ์–ด ๋ฒ„๋ฒ…์ด๊ฑฐ๋‚˜ ํ•˜๋Š” ํ˜„์ƒ์ด ์ƒ๊น๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋„์ปค๋ฅผ ์ด์šฉํ•˜์—ฌ Database๋ฅผ ์˜ฌ๋ ธ๋‹ค๊ฐ€ ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด ๋‚ด๋ฆฌ๊ณ  ํ•˜๋Š”๋ฐ ์ด๊ฒƒ๋„ ๋„ˆ๋ฌด ๋ฒˆ๊ฑฐ๋กญ๊ณ  ์ž์นซํ•˜๋ฉด ๊ณ„์† ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ๋‚จ์•„์žˆ์–ด ๋ฆฌ์†Œ์Šค๋ฅผ ์žก์•„๋จน์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋‚˜์˜จ๊ฒŒ TestContainers์ž…๋‹ˆ๋‹ค.
TestContainers๋Š” ํ…Œ์ŠคํŠธ ์ด์ „์— H2, PostgreSQL๋“ฑ Docker Container๋ฅผ ๋”ฐ๋กœ ๋„์šฐ์ง€ ์•Š์•„๋„ ์ž๋™์œผ๋กœ ํ…Œ์ŠคํŠธํ• ๋•Œ DB Contatiner๋ฅผ ์ž๋™์œผ๋กœ ๋„์›Œ์ฃผ๋Š” ์—ญํ• ์„ ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

ํ•ด๋‹น ์˜ˆ์ œ๋Š” Kotlin, KotlinDSL(gradle)๋กœ ์ž‘์„ฑ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

Gradle ์„ค์ •๋ฒ•

extra["testcontainersVersion"] = "1.14.3"

dependencies {
    /* testcontainers */
    testCompile("org.testcontainers:postgresql:1.14.3")
    testImplementation("org.testcontainers:junit-jupiter")
    
    /* ๊ธฐํƒ€ ์˜ˆ์ œ์— ํ•„์š”ํ•œ ์˜์กด์„ฑ */    
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }

}

dependencyManagement {
    imports {
        mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}")
    }
}

์œ„์—๋Š” postgresql์„ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ๋กœ org.testcontainers:postgresql๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค. Postgresql์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค๋ฅผ ์„ค์ •ํ• ๋•Œ๋Š” ๋‹ค์Œ ๋งํฌ์—์„œ ์ฐธ์กฐํ•˜์—ฌ ์•Œ๋งž๊ฒŒ ๋„ฃ์–ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋ชจ๋“ˆ๋ฆฌ์ŠคํŠธ์— ์—†๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ๋Š” ํ•˜๋‹จ์— ์ถ”๊ฐ€์ ์œผ๋กœ ์ž‘์„ฑํ•ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

Test Properties

application-test.properties

spring.datasource.url=jdbc:tc:postgresql:///test-database
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.jpa.hibernate.ddl-auto=create-drop

๊ธฐ์กด์— ์กด์žฌํ•˜๋˜ Hostname๊ณผ Port ์ •๋ณด๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ์ •๋ณด๋ผ ๊ธฐ์ž…ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ํ…Œ์ŠคํŠธ์‹œ์—๋งŒ ์ž ๊น ์กด์žฌํ•˜๊ณ  ์‚ฌ๋ผ์ง€๋Š” ํ˜•ํƒœ๋ผ Username, Password ์ •๋ณด๋„ ํ•„์š”๊ฐ€ ์—†์–ด ๋ช…์‹œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. tc๋ผ๋Š” ํ‚ค์›Œ๋“œ๋ฅผ ๊ฐ€์ง„ URL์„ JDBC ๋“œ๋ผ์ด๋ฒ„๊ฐ€ ์‚ฌ์šฉํ•˜๋„๋ก spring.datasource.driver-class-name์— ContainerDatabaseDriver๋ฅผ ์„ค์ •ํ•˜์—ฌ ์ค๋‹ˆ๋‹ค.

Code

๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋กœ ํšŒ์›์ •๋ณด๋ฅผ ๋‹ด๋Š” Member์™€ ํšŒ์›์ •๋ณด๋ฅผ ์ €์žฅํ•  MemberRepository๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

// Member.kt
@Entity
class Member(
    @Id @GeneratedValue
    var id: Long? = null,
    var name: String? = null,
    var email: String? = null,
    var password: String? = null
)

// MemberRepository.kt
interface MemberRepository : JpaRepository<Member, Long>
// MemberRepositoryTest.kt
package com.ssabae.container.test

import com.ssabae.container.test.domain.Member
import com.ssabae.container.test.domain.MemberRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Testcontainers

@DataJpaTest
class MemberRepositoryTest {

    companion object {

        @JvmStatic
        private val postgresqlContainer = PostgreSQLContainer<Nothing>("postgres:latest").apply {
            withDatabaseName("test-database")
        }

        @BeforeAll
        @JvmStatic
        fun beforeAll() {
            postgresqlContainer.start()
        }

        @AfterAll
        @JvmStatic
        fun afterAll() {
            postgresqlContainer.stop()
        }
    }

    @Autowired
    lateinit var memberRepository: MemberRepository

    @Test
    fun test() {
        val member = Member(
            email = "email",
            name = "name",
            password = "password"
        )
        val save = memberRepository.save(member)
        assertThat(save.email).isEqualTo("email")
        assertThat(save.name).isEqualTo("name")
        assertThat(save.password).isEqualTo("password")
    }
}

PostgreSQLContainer์—์„œ ์„ค์ •ํ•œ DatabaseName์€ properties์—์„œ ๋ช…์‹œํ•œ DatabaseName๊ณผ ์ผ์น˜ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

PostgreSQLContainer๊ฐ€ static์ด ์•„๋‹ˆ๋ผ๋ฉด ๊ฐ๊ฐ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์–ด ์†๋„๊ฐ€ ๋งค์šฐ ๋А๋ ค์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

@BeforeEach๋ฅผ ์ด์šฉํ•˜์—ฌ ๋งค ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ๋‚ด๋ถ€์— ์žˆ๋Š” Entity๋“ค์„ ์‚ญ์ œํ•˜์—ฌ์ค๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด JUnit5์—์„œ ์ œ๊ณตํ•˜๋Š” beforeAll, afterAll์—์„œ Contatiner๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๋กœ์ง์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@DataJpaTest
@Testcontainers
class MemberRepositoryTest {
    
    companion object {
        @JvmStatic
        @Container
        private val postgresqlContainer = PostgreSQLContainer<Nothing>("postgres:latest").apply {
            withDatabaseName("test-database")
        }
    }
    
    @Autowired
    lateinit var memberRepository: MemberRepository

    @BeforeEach
    fun beforeEach() {
        memberRepository.removeAll()
    }
}

@Testcontainers + @Container ๋‘๊ฐœ์˜ Annotation์œผ๋กœ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฏธ์ง€์› ๋ชจ๋“ˆ ์„ค์ • ๋ฐฉ๋ฒ•

@DataJpaTest
@Testcontainers
class MemberRepositoryTest {

    companion object {
        @JvmStatic
        @Container
        private val h2Container = GenericContainer<Nothing>("oscarfonts/h2").apply {
            withExposedPorts(1521)
        }
    }
}

GenericContainer๋Š” ์œ„์—์„œ ๋ดค๋˜ PostgreSQLContainer์˜ ์Šˆํผํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. ์ƒ์„ฑ์ž์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋กœ์ปฌ์— ์„ค์น˜๋˜์–ด์žˆ๋Š” ์ด๋ฏธ์ง€๋ฅผ ๋จผ์ € ๊ฒ€์ƒ‰ํ•˜๊ณ  ์—†์œผ๋ฉด Docker Hub์—์„œ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์•„์„œ ์‹คํ–‰ํ•ด์ค๋‹ˆ๋‹ค. GenericContainer๋กœ Container๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๊ฒŒ ๋œ๋‹ค๋ฉด ๋ณ„๋„์˜ properties๊ฐ€ ์—†์–ด๋„ ๋ฉ๋‹ˆ๋‹ค.

Contatiner Logging

class MemberRepositoryTest {

    companion object {

        private val LOGGER = LoggerFactory.getLogger(MemberRepositoryTest::class.java)

        @JvmStatic
        @Container
        private val h2Container = GenericContainer<Nothing>("oscarfonts/h2").apply {
            withExposedPorts(1521)
        }

        @BeforeAll
        @JvmStatic
        fun beforeAll() {
            val logConsumer = Slf4jLogConsumer(LOGGER)
            h2Container.followOutput(logConsumer)
        }
    }
    
    @Test
    fun test() {
        val member = Member(
            email = "email",
            name = "name",
            password = "password"
        )
        val save = memberRepository.save(member)
        assertThat(save.email).isEqualTo("email")
        assertThat(save.name).isEqualTo("name")
        assertThat(save.password).isEqualTo("password")

        LOGGER.debug("logs : ${h2Container.logs}")
    }

}

Container์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” followOutput ํ•จ์ˆ˜์— Logger๋ฅผ ๋„ฃ๊ฑฐ๋‚˜ Lambda์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ์•„์›ƒํ’‹ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ getLog() ๋ฅผ ์ด์šฉํ•˜์—ฌ ์Œ“์—ฌ์žˆ๋˜ ๋กœ๊ทธ๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Docker Compose์—์„œ์˜ ์‚ฌ์šฉ

Docker Compose์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ๋Š” docker-compose.yml ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ฃผ๋ฉด์„œ ์‹œ์ž‘์ด ๋ฉ๋‹ˆ๋‹ค.

// docker-compose.yml
version: "3"

services:
  study-db:
    image: postgres
    ports:
      - 5432
    environment:
      POSTGRES_DB: database_name
      POSTGRES_USER: database_user
      POSTGRES_PASSWORD: database_password

docker-compose.yml ํŒŒ์ผ์€ src/test/resources/ ์•„๋ž˜์— ์žˆ๊ฒŒ ํ•˜๋Š”๊ฒŒ ๊ฐ€์žฅ ๊ด€๋ฆฌํ•˜๊ฒŒ ํŽธํ•˜๊ณ  ์ข‹์€ ์œ„์น˜์ธ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// MemberRepositoryTest.kt
@DataJpaTest
@Testcontainers
class MemberRepositoryTest {

    companion object {

        @JvmStatic
        @Container
        private val container = 
            DockerComposeContainer<Nothing>(
            	File("src/test/resources/docker-compose.yml")
            )
    }
    ...
}

์œ„์™€๊ฐ™์ด DockerComposeContainer์—์„œ File๋กœ ymlํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ ๋„ฃ์–ด์ฃผ๋ฉด ์ž๋™์ ์œผ๋กœ docker-compose๊ฐ€ ์ ์šฉ์ด ๋˜๋ฉด์„œ contatiner๊ฐ€ ๋œจ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋‹จ์ ?

๋‹จ์ ์ด๋ผ๊ณ ํ•˜๋ฉด ๋„์ปค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ค€๋น„ํ•˜๊ณ  ์‹คํ–‰ํ•˜๋Š” ์‹œ๊ฐ„์ด ์ข€ ์˜ค๋ž˜ ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค. ๋กœ์ปฌ์—์„œ๋งŒ ํ•ด์•ผํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๋น ๋ฅด๊ณ  ๊ฐ„ํŽธํ•˜๊ฒŒ ํ•˜๊ธฐ์œ„ํ•ด์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์„ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์„ค์ •ํ•˜๋Š”๋ฒ•๋„ ๊ฐ„๋‹จํ•˜๊ณ  ์ ์šฉํ•ด๋ณด๋Š”๋ฐ ์‹œ๊ฐ„๋„ ์–ผ๋งˆ ์•ˆ๋“ค์–ด๊ฐ€ ๊ฒฝํ—˜์‚ผ์•„ ๋„์ž…์„ ํ•ด๋ณด์‹œ๋Š”๊ฒƒ๋„ ๋‚˜์˜์ง€๋Š” ์•Š์€๊ฑฐ๋ผ ์ƒ๊ฐ๋ฉ๋‹ˆ๋‹ค.