
보통 일반적인 테스트는 다음과 같은 방식으로 진행할 것이다.
위 세 가지 방식은 테스트에서 중요한 멱등성이 깨지고 데이터를 계속 clean 해줘야 하는 문제가 있다.
그렇다고 Mocking을 한다면 DB와의 통신을 건너 뛰고 순수한 로직만을 테스트할 순 있지만 실제 환경에서 DB와 연동했을 때 문제가 발생할 가능성이 높다.
Testcontainers는 Docker 컨테이너에 래핑된 실제 서비스를 사용하여 로컬 개발 및 테스트 종속성을 부트스트랩하기 위한 쉽고 가벼운 API를 제공하는 라이브러리입니다. Testcontainers를 사용하면 모의 서비스나 메모리 내 서비스 없이 프로덕션에서 사용하는 것과 동일한 서비스에 의존하는 테스트를 작성할 수 있습니다.
(공식 reference 번역)
Testcontainers를 사용하면, 도커 컨테이너를 Java 코드로 특정 도커 이미지를 실행하고 끌 수 있다. Test 환경에 도입하면 In-Memory DB로 테스트 하는 것이 아닌 실제 운영 DB와 동일한 환경을 외부 인스턴스에 띄우지 않고 로컬 도커 컨테이너에서 테스트할 때마다 DB 컨테이너를 띄우고 테스트 종료 시 컨테이너를 내리는 작업을 자동화 할 수 있다.
장점
단점
먼저 build.gradle.kts에 의존성을 추가한다.
// 중략
dependencies {
// testcontainers
testImplementation ("org.springframework.boot:spring-boot-testcontainers")
testImplementation ("org.testcontainers:mysql:1.19.6")
testImplementation ("org.testcontainers:junit-jupiter")
}
테스트 모듈에 TestContainer를 위한 설정을 다음과 같이 작성한다.
공식 문서의 내용과는 다르게, Spring과 integration 하기 위해 직접 생명 주기를 제어하는 방식으로 처리한다.
이렇게 처리하면 테스트 클래스 생성 마다 abstract class 매번 상속받거나 매번 container를 선언할 필요 없이 test의 생명 주기와 일치시킬 수 있다.
package me.ramos.guide.config
import jakarta.annotation.PreDestroy
import org.springframework.boot.test.context.TestConfiguration
import org.testcontainers.containers.MySQLContainer
import org.testcontainers.junit.jupiter.Container
@TestConfiguration("TestMySQLContainer")
class TestMySQLContainer {
@PreDestroy
fun stop() {
container.stop()
}
companion object {
@Container
@JvmStatic
val container = MySQLContainer<Nothing>("mysql:8.0.19")
.apply {
withDatabaseName("test")
withUsername("root")
withPassword("root")
}
.apply {
start()
}
}
}
DataSource를 설정하는 부분은 다음과 같다.
package me.ramos.guide.config
import com.zaxxer.hikari.HikariDataSource
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.DependsOn
@TestConfiguration
class DataSourceConfig {
@Bean
// 컨테이너가 생성된 이후 빈이 생성되도록 DependsOn을 적용한다.
@DependsOn("TestMySQLContainer")
fun dataSource(): HikariDataSource {
return DataSourceBuilder.create()
.type(HikariDataSource::class.java)
.url(TestMySQLContainer.container.jdbcUrl)
.username(TestMySQLContainer.container.username)
.password(TestMySQLContainer.container.password)
.build()
}
}
// test/resources/application.yaml
spring:
datasource:
url: jdbc:tc:mysql:8.0.19//guide
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
show_sql: true
package me.ramos.guide.repository
import me.ramos.guide.config.DataSourceConfig
import me.ramos.guide.config.TestConfig
import me.ramos.guide.config.TestMySQLContainer
import me.ramos.guide.domain.Team
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
import org.springframework.context.annotation.Import
import org.springframework.data.repository.findByIdOrNull
@DataJpaTest
@Import(TestConfig::class, TestMySQLContainer::class, DataSourceConfig::class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
internal class TeamRepositoryTest @Autowired constructor(
val entityManager: TestEntityManager,
val teamRepository: TeamRepository
) {
@Test
fun findById() {
val team = Team("Real Madrid C.F", "La Liga")
entityManager.persist(team)
entityManager.flush()
val found = teamRepository.findByIdOrNull(team.id)
assertThat(found).isEqualTo(team)
assertThat(found?.name).isEqualTo(team.name)
assertThat(found?.league).isEqualTo(team.league)
}
}
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE): H2 데이터베이스 대신 Testcontainer를 적용하도록 변경참고로, 테스트를 실행 시 반드시 로컬에 Docker engine이 실행중이어야 한다.