이 글은 이전 글 Testcontainers 사용하기까지 에서 이어졌습니다. 잘못된 내용이나 의견 있다면 편하게 말씀해주세요 🙏🏻
Testcontainers 간단 소개
Testcontainers 라이브러리는 도커 컨테이너를 자바 코드로 조작할 수 있다. 다시 말해 자바 코드로 특정 도커 이미지를 실행하고 끌 수 있다. 일반 도커 컨테이너와 마찬가지로 네트워크 통신, 스토리지 조작, 환경변수 설정을 할 수 있다.
이를 DB 테스트 환경에 도입하면 다음과 같다. 인메모리 DB로 테스트하는 것이 아닌 실제 운영 DB와 동일한 환경을 외부 인스턴스에 띄우지 않고 로컬 도커 컨테이너에서 테스트할 때마다 DB 컨테이너를 띄우고 테스트가 끝나면 컨테이너를 내리는 작업을 자동화할 수 있다. CI 환경에 도커만 설치되어 있다면 간단히 테스트할 수 있다. 또한 별도의 도커 컨테이너 관리를 하지 않아도 독립적인 환경을 구축할 수 있는 강점을 가지고 있다. 또한 JUnit 계열과 합쳐져 테스트 코드 작성하기 간편하다.
Testcontainers 패키지 구조
Testcontainers는 자바 코드로 도커 컨테이너를 조작할 수 있다. 퓨어한 컨테이너 조작은 GenericContainer() 메서드를 사용해 조작할 수 있다.
// Testcontainers - JUnit 5 Quickstart : https://www.testcontainers.org/quickstart/junit_5_quickstart/
public static final DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:3.0.2");
public static GenericContainer<?> alpine = new GenericContainer<>(ALPINE_IMAGE)
.withExposedPorts(80)
.withEnv("MAGIC_NUMBER", "42")
.withCommand("/bin/sh", "-c",
"while true; do echo \"$MAGIC_NUMBER\" | nc -l -p 80; done");
JdbcDatabaseContainer는 도커 이미지 중 DB 관련을 손쉽게 사용하기 위한 sub class이고 MySQL에 특화된 sub class는 MySQLContainer이다.
Testcontainers 환경 설정
- 도커 설치
Testcontainers는 로컬 도커를 사용한다. 따라서 도커가 설치되어있어야 한다.
로컬 도커가 없는 상태에서 라이브러리를 사용하면 아래와 같은 에러가 발생한다.
Could not find a valid Docker environment. Please see logs and check configuration
java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$6(DockerClientProviderStrategy.java:242)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)
at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:234)
at org.testcontainers.DockerClientFactory.getOrInitializeStrategy(DockerClientFactory.java:135)
// 생략
- JUnit4 또는 JUnit5 의존성 추가
- Testcontainers 의존성 추가(Gradle)
// test Containers
testImplementation "org.testcontainers:junit-jupiter:1.17.2"
testImplementation "org.testcontainers:mysql:1.17.2" // mysql 컨테이너를 사용한다면 추가
// DB Driver
runtimeOnly 'mysql:mysql-connector-java'
- Testcontainers 권장 logback 설정
공식 문서에선 테스트 환경 test/resources 에 logback-test.xml 파일을 만들어 줄 것을 권장한다(root level=info). (해당 로그백을 설정하지 않으면 수 백 줄의 DEBUG가 모두 나와 로그를 알아보기 힘들다.)
기본 사용법 1 (@BeforeEach - start(), @AfterEach - stop())
MySQLContainer 객체를 생성한다. 인자로 mysql image 버전을 명시해준다.
- mysqlContainer.start()으로 컨테이너를 시작한다
- mysqlContainer.stop()으로 컨테이너를 종료한다.
만들어진 MySQLContainer 객체는 해당 컨테이너의 정보를 조회할 수 있는 메서드를 제공한다(getJdbcUrl(), getHost()...)
@Slf4j
@Testcontainers
@DisplayName("@Container 어노테이션 없이 가장 기본 사용 방법, 직접 start(), stop()으로 매 테스트마다 도커 띄우기")
class BasicMySQLContainer {
MySQLContainer mySQLContainer = new MySQLContainer("mysql:8"); // MySQLContainer 객체 생성
@BeforeEach
void setUp() {
mySQLContainer.start(); // 매 테스트 시작 시 컨테이너 시작
}
@AfterEach
void tearDown() {
mySQLContainer.stop(); // 매 테스트 끝난 뒤 컨테이너 종료
}
@Test
void test1() {
log.info("로그 getJdbcDriverInstance {} ", mySQLContainer.getJdbcDriverInstance());
log.info("로그 getJdbcUrl {} ", mySQLContainer.getJdbcUrl());
log.info("로그 getMappedPort {} ", mySQLContainer.getMappedPort(3306));
log.info("로그 getHost {} ", mySQLContainer.getHost());
log.info("로그 getUsername {} ", mySQLContainer.getUsername());
log.info("로그 getPassword {} ", mySQLContainer.getPassword());
}
@Test
void test2() {
log.info("로그 getJdbcDriverInstance {} ", mySQLContainer.getJdbcDriverInstance());
log.info("로그 getJdbcUrl {} ", mySQLContainer.getJdbcUrl());
log.info("로그 getMappedPort {} ", mySQLContainer.getMappedPort(3306));
log.info("로그 getHost {} ", mySQLContainer.getHost());
log.info("로그 getUsername {} ", mySQLContainer.getUsername());
log.info("로그 getPassword {} ", mySQLContainer.getPassword());
}
}
test1(), test2() 두 개의 테스트가 실행된다. BeforeEach, AfterEach가 각 두 번씩 실행된다. 이는 컨테이너가 두 번 켜지고 꺼진다.
먼저 testcontainers/ryuk:0.3.3 컨테이너가 켜진다. 이는 testcontainer 라이브러리가 docker 컨테이너를 관리하기 위한 컨테이너이다. 60812 포트로 mysql 8이 하나 뜨고 꺼진다. 이후 60946 포트로 mysql 8이 또 뜨고 꺼진다
이렇게 간단히 mysql8 컨테이너를 테스트 개수마다 끄고 켰다.
문제가 있다. BeforeEach, AfterEach에 start(), stop()을 매번 작성해줘야 하는 번거로움이 있다.
기본 사용법 2 (start(), stop() 을 @Container로 대체하기)
MySQLContainer 위에 @Container를 사용하면 매번 start(), stop() 작성하지 않아도 된다.
class Basic2MySQLContainer {
@Container
MySQLContainer mySQLContainer = new MySQLContainer("mysql:8");
// 생략
}
반복 코드는 @Container으로 대체했다.
문제가 또 있다. @Test, 테스트 메서드마다 컨테이너가 매번 뜨고 꺼진다. 테스트가 많다면 매번 컨테이너를 끄고 켜는데 많은 시간이 걸린다.
기본 사용법 3 (컨테이너 한 번만 띄우기)
static을 붙여 사용하면 컨테이너 객체를 생성하면 테스트 메서드 단위 -> 클래스 단위로 실행 범위가 바뀐다. 따라서 매번 컨테이너를 실행하지 않을 수 있다.
class Basic3MySQLContainer {
@Container
static MySQLContainer mySQLContainer = new MySQLContainer("mysql:8");
// 생략
}
이를 전체 테스트 코드에서도 활용할 수 있다. MySQLContainer를 별도의 클래스, static으로 만든 뒤 해당 DB가 필요한 테스트에서 이를 상속받아 사용할 수 있다.
@Testcontainers
public class MysqlTestContainer {
private static final String MYSQL_VERSION = "mysql:8";
@Container
public static final MySQLContainer MYSQL_CONTAINER = new MySQLContainer(MYSQL_VERSION)
}
@SpringBootTest
class LoginAcceptanceTest extends MysqlTestContainer {
// 생략
}
기본 사용법 4 (MySQL 컨테이너 커스텀)
제공되는 메서드로 MySQL의 db 이름, 유저, 비밀번호, conf.d, init.sql 등 을 초기 실행에 넣어줄 수 있다.
@Slf4j
@Testcontainers
@DisplayName("MySQL 컨테이너 커스텀 설정")
class CustomMySQLContainer {
@Container
static JdbcDatabaseContainer mySQLContainer = new MySQLContainer("mysql:8")
.withConfigurationOverride("learning.testcontainers/custom.conf.d")
.withDatabaseName("customdb")
.withUsername("kukim")
.withPassword("1234")
.withInitScript("learning.testcontainers/init.sql");
@Test
void test1() {
log.info("test 1 로그 getJdbcDriverInstance {} ", mySQLContainer.getJdbcDriverInstance());
log.info("test 1 로그 getJdbcUrl {} ", mySQLContainer.getJdbcUrl());
log.info("test 1 로그 getMappedPort {} ", mySQLContainer.getMappedPort(3306));
log.info("test 1 로그 getHost {} ", mySQLContainer.getHost());
log.info("test 1 로그 getUsername {} ", mySQLContainer.getUsername());
log.info("test 1 로그 getPassword {} ", mySQLContainer.getPassword());
}
}
cnf.d와 init.sql의 classpath는 /test/resources이다.
실행 결과는 아래와 같다.
23:39:31.305 [Test worker] INFO dev.kukim.learning.testcontainers.mysqlcontainer.CustomMySQLContainer - test 1 로그 getJdbcDriverInstance com.mysql.cj.jdbc.Driver@24d61e4
23:39:31.305 [Test worker] INFO dev.kukim.learning.testcontainers.mysqlcontainer.CustomMySQLContainer - test 1 로그 getJdbcUrl jdbc:mysql://localhost:61135/customdb
23:39:31.305 [Test worker] INFO dev.kukim.learning.testcontainers.mysqlcontainer.CustomMySQLContainer - test 1 로그 getMappedPort 61135
23:39:31.305 [Test worker] INFO dev.kukim.learning.testcontainers.mysqlcontainer.CustomMySQLContainer - test 1 로그 getHost localhost
23:39:31.305 [Test worker] INFO dev.kukim.learning.testcontainers.mysqlcontainer.CustomMySQLContainer - test 1 로그 getUsername kukim
23:39:31.306 [Test worker] INFO dev.kukim.learning.testcontainers.mysqlcontainer.CustomMySQLContainer - test 1 로그 getPassword 1234
기본적인 Testcontainers 사용방법을 알아봤다. 라이브러리를 사용해 MySQL 도커를 띄웠고 해당 정보도 확인했다. JDBC, JPA에서 이를 어떻게 주입받아 사용할까?
간단한 방법으로는 이미 MySQLContainer.getJdbcUrl(), getHost()... 메서드를 통해 도커의 정보를 알 수 있으니 직접 커넥션을 만들어 접속할 수도 있다.
try {
Connection conn = DriverManager.getConnection(mySQLContainer.getJdbcUrl(),
mySQLContainer.getUsername(),
mySQLContainer.getPassword());
} catch (SQLException e) {
e.printStackTrace();
}
위 방법도 좋겠지만 설정 파일(application.properties,. yml)을 통해 Testcontainers는 드라이버를 입력하여 손쉽게 접근할 수 있다.
테스트 환경에서 Testcontainers 연결하기
아래와 같이 설정하면 된다.
- driver-class-name이 일반 MySQL 드라이버가 아닌 testcontainer 드라이버이다.
- 'tc'를 붙이면 알아서 url 중 hostname을 생략할 수 있다.(localhost:port)
// test/resources/application.yml
spring:
datasource:
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
url: jdbc:tc:mysql:8://customdb
username: kukim
password: 1234
보다 자세한 내용 (Testcontainers - MySQL Module Docs)
⛓ Reference
Testcontainers - MySQL Module Docs
'✅ 테스트' 카테고리의 다른 글
[의사결정] Testcontainers 사용하기까지 (0) | 2022.06.21 |
---|---|
JUnit5에서 검증문이 중간에 실패해도 멈추지 않고 검증문 모두 실행하기(AssertAll, AssertJ, SoftAssertions) (0) | 2022.06.02 |
단위 테스트란 무엇일까? 런던파와 고전파의 차이점 🆚 (2) | 2022.03.19 |
IntelliJ의 Code&Live Templates 활용하여 생산성 높이기! 테스트코드 작성시간 줄이고 아직 구현하지 않은 메서드 예외로 확인하기 (0) | 2022.03.10 |
private 메서드도 테스트를 해야 할까? (private 메서드 테스트 하고 싶을 때...) ✅ 👃 (7) | 2022.02.16 |
댓글