TestContainers 란?
TestContainers는 Docker 컨테이너를 활용하여 실제 서비스와의 통합 테스트를 쉽게 수행할 수 있도록 지원하는 테스트 라이브러리입니다.
많은 스프링 사용자들이 Test를 할 때 인메모리 데이터베이스(H2)를 사용하는데 개발, 프로덕션 단계에서는 H2가 아닌 PostgreSQL, MySQL을 사용합니다.
큰 문제는 없을 수 있으나 테스트의 신뢰성이 저하되는 건 부정할 수 없는 사실입니다.
실제로 SQL 호환성 및 데이터베이스 기능에서 몇 가지 차이점이 있습니다.
이러한 단점을 극복하기 위해 Test Containers를 사용할 수 있습니다.
TestContainers는 다음과 같은 장점을 가지고 있습니다.
- 일관성 : 테스트 컨테이너를 사용하면 개발, 테스트, 프로덕션 단계 전반에 걸쳐 일관된 환경을 보장할 수 있습니다.
- 격리 : 각 테스트는 자체 격리된 컨테이너에서 실행될 수 있어 테스트 간의 간섭을 방지합니다.
- 효율성 : 테스트 컨테이너는 빠르게 부팅되어 테스트 환경 설정에 소요되는 시간을 크게 줄입니다.
또한 다양한 모듈을 제공하는 것도 TestContainers의 큰 장점입니다.
하기의 링크를 보면 69개의 모듈을 제공하는 것을 알 수 있습니다. (MySQL, Redis, PostgreSQL, Google Cloud 등)
https://testcontainers.com/modules/?language=java
TestContainers의 단점?
이렇게만 본다면 TestContainers은 테스트에서 꼭 필요한 프레임워크처럼 보일 수 있습니다.
하지만 잘 알아보지 않고 사용한다면 모든 테스트 클래스에서 Docker에 해당 모듈의 이미지를 빌드해 많은 시간이 걸릴 수 있습니다.
테스트를 통해 알아보겠습니다.
다음과 같이 Container를 정의하고 매우 간단한 테스트가 있는 클래스 6개를 만들겠습니다.
그런 뒤 테스트를 전부 실행시켜 보면 52초가 나옵니다.
분명 단위테스트 또는 인메모리 데이터베이스로 진행하면 5초 이내로 실행될 매우 간단한 테스트인데 오래 걸리는 것 같습니다.
그러면 StopWatch를 통해 어디에서 시간이 오래 걸리는지 파악해 보겠습니다.
우선 @Container 어노테이션은 삭제하겠습니다.
- Container 어노테이션은 테스트 컨테이너를 자동으로 관리하는데, 시간을 측정하려면 수동으로 관리를 해야 합니다. (start, stop)
static StopWatch stopWatch = new StopWatch();
static MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.0.31");
@BeforeAll
static void beforeAll() {
stopWatch.start("MySQL Container start");
mySQLContainer.start();
stopWatch.stop();
System.out.println("MySQLContainer start time: " + stopWatch.getTotalTimeMillis() + " ms");
}
먼저 시작하는 데 걸리는 시간을 알아보겠습니다.
시작을 하는데 8,000ms 즉 8초가 걸리는 것을 알 수 있습니다.
이는 Test Container의 start 메서드를 통해 docker에 컨테이너를 생성 및 시작하기 때문에 오래 걸리는 것입니다.
@AfterAll
static void afterAll() {
stopWatch.start("MySQLContainer Stop");
mySQLContainer.stop();
stopWatch.stop();
System.out.println("MySQLContainer stop time: " + stopWatch.getTotalTimeMillis() + " ms");
}
이번에는 반대로 종료하는 데 걸리는 시간을 알아보겠습니다.
stop은 컨테이너를 종료하기 때문에 start에 비해 시간이 적게 걸립니다.
다시 테스트로 돌아와서 왜 52초가 걸렸는지 살펴보겠습니다.
밑의 로그를 보시면 총 6개의 MySQLContainer start time 기록이 있는 것을 볼 수 있습니다.
각각 7초, 7초, 7초, 7초, 7초, 7초가 걸렸습니다. 이를 합산하면 42초입니다.
총테스트를 실행시키는데 52초가 걸렸다고 하면 10초를 제외한 42초 동안 컨테이너를 Docker에 빌드 및 실행한 것입니다.
(테스트 실행, 컨테이너 종료를 제외하면 거의 대부분을 TestContainers 실행에 사용했다 하더라도 과언이 아닙니다)
@Container 어노테이션을 적용해도 동일하게 52초가 나옵니다.
MySQLContainer start time: 6976 ms
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
2024-08-15T17:50:06.022+09:00 INFO 97342 --- [demo] [ Test worker] test.example.MySQLContainer1 : Starting MySQLContainer1 using Java 17.0.11 with PID 97342 (started by yongjunhong in /Users/yongjunhong/Desktop/FixtureMonkey Test)
2024-08-15T17:50:06.022+09:00 INFO 97342 --- [demo] [ Test worker] test.example.MySQLContainer1 : No active profile set, falling back to 1 default profile: "default"
2024-08-15T17:50:06.055+09:00 INFO 97342 --- [demo] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2024-08-15T17:50:06.055+09:00 INFO 97342 --- [demo] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2024-08-15T17:50:06.059+09:00 INFO 97342 --- [demo] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 3 ms. Found 1 JPA repository interface.
2024-08-15T17:50:06.080+09:00 INFO 97342 --- [demo] [ Test worker] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2024-08-15T17:50:06.081+09:00 INFO 97342 --- [demo] [ Test worker] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2024-08-15T17:50:06.086+09:00 INFO 97342 --- [demo] [ Test worker] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2024-08-15T17:50:06.086+09:00 INFO 97342 --- [demo] [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Starting...
2024-08-15T17:50:06.093+09:00 INFO 97342 --- [demo] [ Test worker] com.zaxxer.hikari.pool.HikariPool : HikariPool-2 - Added connection org.testcontainers.jdbc.ConnectionWrapper@32b273c1
2024-08-15T17:50:06.093+09:00 INFO 97342 --- [demo] [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Start completed.
2024-08-15T17:50:06.093+09:00 WARN 97342 --- [demo] [ Test worker] org.hibernate.orm.deprecation : HHH90000025: MySQL8Dialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2024-08-15T17:50:06.093+09:00 WARN 97342 --- [demo] [ Test worker] org.hibernate.orm.deprecation : HHH90000026: MySQL8Dialect has been deprecated; use org.hibernate.dialect.MySQLDialect instead
2024-08-15T17:50:06.109+09:00 INFO 97342 --- [demo] [ Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-08-15T17:50:06.117+09:00 INFO 97342 --- [demo] [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-08-15T17:50:06.134+09:00 INFO 97342 --- [demo] [ Test worker] test.example.MySQLContainer1 : Started MySQLContainer1 in 0.127 seconds (process running for 16.249)
2024-08-15T17:50:06.164+09:00 DEBUG 97342 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T17:50:06.333+09:00 INFO 97342 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer2]: MySQLContainer2 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T17:50:06.337+09:00 INFO 97342 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer2
2024-08-15T17:50:06.338+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Creating container for image: mysql:8.0.31
2024-08-15T17:50:06.379+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 is starting: 4c99d15dc8353bdb5834872babd3d1a055aa7a3f36089cd4ea3c6180d8d9082e
2024-08-15T17:50:06.519+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Waiting for database connection to become available at jdbc:mysql://localhost:55923/test using query 'SELECT 1'
2024-08-15T17:50:13.254+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 started in PT6.91563S
2024-08-15T17:50:13.254+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container is started (JDBC URL: jdbc:mysql://localhost:55923/test)
MySQLContainer start time: 6916 ms
2024-08-15T17:50:13.264+09:00 DEBUG 97342 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
> Task :test
2024-08-15T17:50:13.404+09:00 INFO 97342 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer3]: MySQLContainer3 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T17:50:13.415+09:00 INFO 97342 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer3
2024-08-15T17:50:13.417+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Creating container for image: mysql:8.0.31
2024-08-15T17:50:13.456+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 is starting: 58f2df24907f19396e6960ddf44f03d17348470045c512fb7b7da9ceefc1498b
2024-08-15T17:50:13.576+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Waiting for database connection to become available at jdbc:mysql://localhost:55999/test using query 'SELECT 1'
2024-08-15T17:50:20.343+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 started in PT6.926603S
2024-08-15T17:50:20.343+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container is started (JDBC URL: jdbc:mysql://localhost:55999/test)
MySQLContainer start time: 6927 ms
2024-08-15T17:50:20.349+09:00 DEBUG 97342 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T17:50:20.490+09:00 INFO 97342 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer4]: MySQLContainer4 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T17:50:20.496+09:00 INFO 97342 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer4
2024-08-15T17:50:20.497+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Creating container for image: mysql:8.0.31
2024-08-15T17:50:20.530+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 is starting: 4a01749481cafb6fc1c9479f38530e13ebf1950837ecf814302e44ff4dba819e
2024-08-15T17:50:20.694+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Waiting for database connection to become available at jdbc:mysql://localhost:56072/test using query 'SELECT 1'
2024-08-15T17:50:27.259+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 started in PT6.761506S
2024-08-15T17:50:27.259+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container is started (JDBC URL: jdbc:mysql://localhost:56072/test)
MySQLContainer start time: 6762 ms
2024-08-15T17:50:27.265+09:00 DEBUG 97342 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T17:50:27.413+09:00 INFO 97342 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer5]: MySQLContainer5 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T17:50:27.420+09:00 INFO 97342 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer5
2024-08-15T17:50:27.422+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Creating container for image: mysql:8.0.31
2024-08-15T17:50:27.455+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 is starting: 6ac14ad2b9c91f3ef8b327464422dc1eff526194151d64c6eb00c4588a1ac282
2024-08-15T17:50:27.568+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Waiting for database connection to become available at jdbc:mysql://localhost:56146/test using query 'SELECT 1'
2024-08-15T17:50:34.555+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 started in PT7.133707S
2024-08-15T17:50:34.556+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container is started (JDBC URL: jdbc:mysql://localhost:56146/test)
MySQLContainer start time: 7135 ms
2024-08-15T17:50:34.569+09:00 DEBUG 97342 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T17:50:34.733+09:00 INFO 97342 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer6]: MySQLContainer6 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T17:50:34.738+09:00 INFO 97342 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer6
2024-08-15T17:50:34.740+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Creating container for image: mysql:8.0.31
2024-08-15T17:50:34.773+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 is starting: 6a1ad36e06437c2d0a974d8328c3e6a50b152479ce004690e8fa492119bb637c
2024-08-15T17:50:35.001+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Waiting for database connection to become available at jdbc:mysql://localhost:56227/test using query 'SELECT 1'
2024-08-15T17:50:41.833+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 started in PT7.092695S
2024-08-15T17:50:41.833+09:00 INFO 97342 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container is started (JDBC URL: jdbc:mysql://localhost:56227/test)
MySQLContainer start time: 7093 ms
시간이 점점 줄어드는 이유로는 Docker Image 캐싱 등이 있을 수 있습니다.
추후에 자세히 다뤄보겠습니다.
하나의 클래스에서 컨테이너를 생성할 때마다 6~7초가 걸린다면 이는 너무나 큰 단점이 될 수 있습니다.
싱글톤을 적용한 TestContainers
이럴 때 적용할 수 있는 개념이 싱글톤입니다.
싱글톤 패턴은 디자인 패턴 중 하나로, 특정 클래스의 인스턴스가 하나만 존재하도록 보장하는 패턴입니다.
TestContainers에 싱글톤을 적용한다면 여러 개의 클래스에서 하나의 테스트 컨테이너로 테스트를 실행하는 것입니다.
공식 문서에서는 다음과 같은 방법을 통해 진행하는 것을 추천하고 있습니다.
공식문서에서는 기본 클래스가 로드될 때 한 번만 실행되는 싱글톤 컨테이너에 대해 설명하고 있습니다.
정적 변수와 메서드는 프로그램이 시작될 때, 즉 클래스가 메모리에 로드될 때 초기화 됩니다.
이는 JVM이 프로그램을 시작하면서 필요한 클래스 정보를 메모리에 로드하는 시점에서 이루어집니다.
그러면 자연스레 정적 메서드 안에서 선언한 MY_SQL_CONTAINER은 클래스가 로드될 때 초기화 되는 것입니다.
정적 변수는 모든 인스턴스에서 공유하는 특징으로 인해 사용자는 추가적인 설정 없이 데이터베이스 환경을 사용할 수 있습니다.
실제로 클래스에 적용시켜 본 결과 52초에서 13초로 39초의 시간이 줄었습니다. (75% 성능 개선)
컨테이너가 시작하는 것이 7초 종료되는 것을 0.2초로 잡았을 때 컨테이너가 한 번만 생성되므로 5번을 줄일 수 있습니다.
* 원래 6번 생성이 되었는데 한 번으로 줄였기 때문에 5번을 줄인 것입니다.
그러면 (7 + 0.2) * 5 = 36이므로 실제로 절약한 시간과 비슷함을 알 수 있습니다. (스프링 실행 시간 제외)
로그에서도 한 번의 컨테이너가 실행된 것을 볼 수 있습니다.
2024-08-15T19:44:55.820+09:00 INFO 26094 --- [demo] [ Test worker] org.testcontainers.DockerClientFactory : ✔︎ Docker server version should be at least 1.6.0
2024-08-15T19:44:55.822+09:00 INFO 26094 --- [demo] [ Test worker] tc.mysql:8.0.31 : Creating container for image: mysql:8.0.31
2024-08-15T19:44:55.859+09:00 INFO 26094 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 is starting: 97f1272f60cb61983591b6039b6fcc492dd3c23ac2726532462626d3a6bb751c
2024-08-15T19:44:55.989+09:00 INFO 26094 --- [demo] [ Test worker] tc.mysql:8.0.31 : Waiting for database connection to become available at jdbc:mysql://localhost:60570/test using query 'SELECT 1'
2024-08-15T19:45:02.797+09:00 INFO 26094 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container mysql:8.0.31 started in PT6.975015S
2024-08-15T19:45:02.797+09:00 INFO 26094 --- [demo] [ Test worker] tc.mysql:8.0.31 : Container is started (JDBC URL: jdbc:mysql://localhost:60570/test)
2024-08-15T19:45:02.805+09:00 INFO 26094 --- [demo] [ Test worker] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.testcontainers.jdbc.ConnectionWrapper@7dfca9e6
2024-08-15T19:45:02.806+09:00 INFO 26094 --- [demo] [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2024-08-15T19:45:02.824+09:00 WARN 26094 --- [demo] [ Test worker] org.hibernate.orm.deprecation : HHH90000025: MySQL8Dialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2024-08-15T19:45:02.824+09:00 WARN 26094 --- [demo] [ Test worker] org.hibernate.orm.deprecation : HHH90000026: MySQL8Dialect has been deprecated; use org.hibernate.dialect.MySQLDialect instead
2024-08-15T19:45:03.172+09:00 INFO 26094 --- [demo] [ Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-08-15T19:45:03.195+09:00 DEBUG 26094 --- [demo] [ Test worker] org.hibernate.SQL : create table product (id integer not null auto_increment, name varchar(255), price integer, primary key (id)) engine=InnoDB
2024-08-15T19:45:03.205+09:00 INFO 26094 --- [demo] [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-08-15T19:45:03.501+09:00 INFO 26094 --- [demo] [ Test worker] test.ApplicationTests : Started ApplicationTests in 8.999 seconds (process running for 9.528)
2024-08-15T19:45:03.825+09:00 INFO 26094 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer1]: MySQLContainer1 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T19:45:03.836+09:00 INFO 26094 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer1
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
2024-08-15T19:45:03.852+09:00 INFO 26094 --- [demo] [ Test worker] test.example.MySQLContainer1 : Starting MySQLContainer1 using Java 17.0.11 with PID 26094 (started by yongjunhong in /Users/yongjunhong/Desktop/FixtureMonkey Test)
2024-08-15T19:45:03.852+09:00 INFO 26094 --- [demo] [ Test worker] test.example.MySQLContainer1 : No active profile set, falling back to 1 default profile: "default"
2024-08-15T19:45:03.885+09:00 INFO 26094 --- [demo] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2024-08-15T19:45:03.885+09:00 INFO 26094 --- [demo] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2024-08-15T19:45:03.889+09:00 INFO 26094 --- [demo] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 3 ms. Found 1 JPA repository interface.
2024-08-15T19:45:03.907+09:00 INFO 26094 --- [demo] [ Test worker] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2024-08-15T19:45:03.908+09:00 INFO 26094 --- [demo] [ Test worker] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2024-08-15T19:45:03.912+09:00 INFO 26094 --- [demo] [ Test worker] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2024-08-15T19:45:03.913+09:00 INFO 26094 --- [demo] [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Starting...
2024-08-15T19:45:03.925+09:00 INFO 26094 --- [demo] [ Test worker] com.zaxxer.hikari.pool.HikariPool : HikariPool-2 - Added connection org.testcontainers.jdbc.ConnectionWrapper@577e2b91
2024-08-15T19:45:03.925+09:00 INFO 26094 --- [demo] [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Start completed.
2024-08-15T19:45:03.926+09:00 WARN 26094 --- [demo] [ Test worker] org.hibernate.orm.deprecation : HHH90000025: MySQL8Dialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2024-08-15T19:45:03.926+09:00 WARN 26094 --- [demo] [ Test worker] org.hibernate.orm.deprecation : HHH90000026: MySQL8Dialect has been deprecated; use org.hibernate.dialect.MySQLDialect instead
2024-08-15T19:45:03.941+09:00 INFO 26094 --- [demo] [ Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-08-15T19:45:03.951+09:00 INFO 26094 --- [demo] [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-08-15T19:45:03.969+09:00 INFO 26094 --- [demo] [ Test worker] test.example.MySQLContainer1 : Started MySQLContainer1 in 0.13 seconds (process running for 9.997)
2024-08-15T19:45:03.989+09:00 DEBUG 26094 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T19:45:04.015+09:00 INFO 26094 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer2]: MySQLContainer2 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T19:45:04.019+09:00 INFO 26094 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer2
2024-08-15T19:45:04.023+09:00 DEBUG 26094 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T19:45:04.030+09:00 INFO 26094 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer3]: MySQLContainer3 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T19:45:04.033+09:00 INFO 26094 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer3
2024-08-15T19:45:04.037+09:00 DEBUG 26094 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T19:45:04.042+09:00 INFO 26094 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer4]: MySQLContainer4 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T19:45:04.047+09:00 INFO 26094 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer4
2024-08-15T19:45:04.050+09:00 DEBUG 26094 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T19:45:04.055+09:00 INFO 26094 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer5]: MySQLContainer5 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T19:45:04.058+09:00 INFO 26094 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer5
2024-08-15T19:45:04.062+09:00 DEBUG 26094 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T19:45:04.068+09:00 INFO 26094 --- [demo] [ Test worker] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [test.example.MySQLContainer6]: MySQLContainer6 does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2024-08-15T19:45:04.071+09:00 INFO 26094 --- [demo] [ Test worker] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration test.Application for test class test.example.MySQLContainer6
2024-08-15T19:45:04.073+09:00 DEBUG 26094 --- [demo] [ Test worker] org.hibernate.SQL : insert into product (name,price) values (?,?)
Hibernate: insert into product (name,price) values (?,?)
2024-08-15T19:45:04.086+09:00 INFO 26094 --- [demo] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-08-15T19:45:04.086+09:00 INFO 26094 --- [demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2024-08-15T19:45:04.087+09:00 INFO 26094 --- [demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
2024-08-15T19:45:04.088+09:00 INFO 26094 --- [demo] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-08-15T19:45:04.088+09:00 INFO 26094 --- [demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Shutdown initiated...
2024-08-15T19:45:04.209+09:00 INFO 26094 --- [demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Shutdown completed.
공식문서에서는 추상 클래스를 통해 상속으로 사용할 수 있다고 했는데 부모 클래스를 상속하는 자식 클래스에서 부모 클래스에 있는 TestContainers 필드를 사용하지 않는다면 추상 클래스로 정의하지 않아도 됩니다.
필자는 MySQLContainer을 생성하는 클래스는 상속할 이유가 없다고 생각해 final 클래스로 정의했습니다.
import org.testcontainers.containers.MySQLContainer;
final class MySqlContainerProvider {
private static final MySQLContainer MY_SQL_CONTAINER;
private static final String IMAGE_VERSION = "mysql:8.0.31";
private MySqlContainerProvider() {
}
static {
MY_SQL_CONTAINER = new MySQLContainer(IMAGE_VERSION);
MY_SQL_CONTAINER.start();
}
}
유의할 점
하나의 컨테이너로 전체 테스트를 진행하기 때문에 데이터 롤백을 잘 신경 쓰셔야 합니다.
@Transaction이 그 방법이 될 수 있고, @AfterEach, @BeforeEach 등 다양한 방법을 고려해서 잘하시길 바라겠습니다.
그리고 모든 모듈이 MySQL Container와 같이 정적 메서드로 간단히 호출할 수 있는 건 아닙니다.
Redis의 경우 System의 properties를 변경해야 되고, localstack은 S3 관련 Bean들을 대체해야 돼서 @Bean으로 등록해야 할 수도 있습니다.
각 모듈의 특징을 잘 알아보고 하셔야 됩니다. 감사합니다.