์์ ์๋ OS์ ์ค์นํ์ฌ ์ฌ์ฉํด์ผํ๋ ๋ง์๊ฒ๋ค์ด ์ด์ ๋ Docker๋ฅผ ์ด์ฉํ์ฌ ์ฝ๊ฒ ์คํํ๊ณ ์ข
๋ฃ์ํฌ ์ ์์ต๋๋ค.
Local ํ๊ฒฝ์์ ํ
์คํธ ํด์ผํ๋ Database๋ ๊ทธ์ค์ ํ๋์ธ๋ฐ ๋ณดํต ์๋น์ค ํํ๋ก ๋ฐฑ๊ทธ๋ผ์ด๋์ ํญ์ ์คํ๋์ด ์์ต๋๋ค.
๊ทธ๋ด๊ฒฝ์ฐ์๋ ๋ฆฌ์์ค ๋ญ๋น๋ ์ฌํ๊ณ ๋ฐฑ๊ทธ๋ผ์ด๋ ์๋น์ค๋ค๋ก ์ธํ์ฌ ์์
ํ๋๋ฐ ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ ์ด ๋ฒ๋ฒ
์ด๊ฑฐ๋ ํ๋ ํ์์ด ์๊น๋๋ค.
๊ทธ๋์ ๋์ปค๋ฅผ ์ด์ฉํ์ฌ Database๋ฅผ ์ฌ๋ ธ๋ค๊ฐ ํ ์คํธ๊ฐ ๋๋๋ฉด ๋ด๋ฆฌ๊ณ ํ๋๋ฐ ์ด๊ฒ๋ ๋๋ฌด ๋ฒ๊ฑฐ๋กญ๊ณ ์์นซํ๋ฉด ๊ณ์ ๋ฐฑ๊ทธ๋ผ์ด๋์ ๋จ์์์ด ๋ฆฌ์์ค๋ฅผ ์ก์๋จน์ต๋๋ค.
๊ทธ๋์ ๋์จ๊ฒ TestContainers์
๋๋ค.
TestContainers๋ ํ
์คํธ ์ด์ ์ H2, PostgreSQL๋ฑ Docker Container๋ฅผ ๋ฐ๋ก ๋์ฐ์ง ์์๋ ์๋์ผ๋ก ํ
์คํธํ ๋ DB Contatiner๋ฅผ ์๋์ผ๋ก ๋์์ฃผ๋ ์ญํ ์ ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค.
ํด๋น ์์ ๋ Kotlin, KotlinDSL(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์ด ์๋ ๋ค๋ฅธ ๋ฐ์ดํฐ ๋ฒ ์ด์ค๋ฅผ ์ค์ ํ ๋๋ ๋ค์ ๋งํฌ์์ ์ฐธ์กฐํ์ฌ ์๋ง๊ฒ ๋ฃ์ด์ฃผ์๋ฉด ๋ฉ๋๋ค.
๋ชจ๋๋ฆฌ์คํธ์ ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ๋ํด์๋ ํ๋จ์ ์ถ๊ฐ์ ์ผ๋ก ์์ฑํด๋์์ต๋๋ค.
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๋ฅผ ์ค์ ํ์ฌ ์ค๋๋ค.
๊ฐ๋จํ ์์ ๋ก ํ์์ ๋ณด๋ฅผ ๋ด๋ 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๊ฐ ์์ด๋ ๋ฉ๋๋ค.
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.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_passworddocker-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๊ฐ ๋จ๊ฒ ๋ฉ๋๋ค.
๋จ์ ์ด๋ผ๊ณ ํ๋ฉด ๋์ปค ์ปจํ ์ด๋๋ฅผ ์ค๋นํ๊ณ ์คํํ๋ ์๊ฐ์ด ์ข ์ค๋ ๊ฑธ๋ฆฝ๋๋ค. ๋ก์ปฌ์์๋ง ํด์ผํ๋ ํ ์คํธ๋ฅผ ๋น ๋ฅด๊ณ ๊ฐํธํ๊ฒ ํ๊ธฐ์ํด์๋ง ์ฌ์ฉํ๋๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค.
ํ์ง๋ง ์ค์ ํ๋๋ฒ๋ ๊ฐ๋จํ๊ณ ์ ์ฉํด๋ณด๋๋ฐ ์๊ฐ๋ ์ผ๋ง ์๋ค์ด๊ฐ ๊ฒฝํ์ผ์ ๋์ ์ ํด๋ณด์๋๊ฒ๋ ๋์์ง๋ ์์๊ฑฐ๋ผ ์๊ฐ๋ฉ๋๋ค.