문제 인식
test환경을 구성하다 local환경에서 db를 MySql을 사용해서 embedded 라이브러리인 wix-embedded-mysql을 사용하려 했지만, 에러 코드를 발생 하기도 했고, 자체 깃헙에서 deprecated된 상태로 TestContainers를 사용하라 명시되어 있었습니다.
따라서 TestContainers를 간단하게 사용해봤던 내용에 대해 공유하고자 합니다
*사용해 보면서 추가적인 업데이트가 있을 예정입니다.
TestContainer가 뭐야?
TestContainer는 Docker 컨테이너를 사용하여 local 및 test의 의존성을 간편하게 설정하고, mock객체나 In Memory서비스 없이도 동일한 서비스에 의존하는 테스트를 만들 수 있다.
WHY?
기본적으로 테스트는 환경, 상황, 실행에 상관 없이 매번 같은 결과가 나와야 한다. 즉 멱등성이 보장된다.
> 환경 구성의 어려움
1. DBMS의 환경부터 생각을 해보면
- DBMS를 각자의 환경에 직접 설치? -> 각자 다른 환경이다.
- Embedded 라이브러리 사용? -> 모든 DBMS가 지원하는가? X
- 예를 들어 Wix-embedded-mysql도 중단된 상태
- In Memory? -> DBMS간 쿼리가 호환하지 않는 경우가 발생
뿐만 아니라 각 서비스별 통합 테스트와 실제 프러덕션 환경에서 유사한 환경으로 실행되어야 하는데, 이러한 환경이 일치하지 못하면, 테스트에서 멱등성이 보장되지 않을 수 있고 테스트에 대한 신뢰도가 떨어지지 않을까 생각됩니다.
2. 외부 시스템과의 의존성
위 그림은 TestContainer의 공식 문서에 나와있는 그림입니다.
여기서 내가 My Service를 기준으로 개발을 진행한다고 하면,
- My Service와 관련된 인프라가 이상 없이 실행 중인가?
- 원하는 상태로 미리 구성되어 있는가?
- My Service와 여러 리소스들이 CI 파이프라인에서 공유되는 경우 다른 리소스에 영향을 주는가?
이러한 문제들을 생각해야 합니다.
WHAT?
따라서 TestContainer는 이러한 문제를 리소스에 종속성을 가지는 새로운 가상 객체를 docker를 통해 만들어서 테스트를 진행할 수 있게 도와줍니다.
HOW?
먼저 testContainer를 사용하기 위한 의존성을 추가해주고
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
Mysql을 사용하기 위해 아래의 의존성을 추가해 주시면 됩니다.
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
다른 모듈을 사용한다면 Database containers - Testcontainers for Java 에서 찾을 수 있습니다.
모듈을 넣었으니 이제 컨테이너를 실행시켜야 겠죠?
먼저 기본 환경부터 확인해 보겠습니다.
@Repository
public class JdbcVoucherRepository implements VoucherRepository{
private final JdbcTemplate jdbcTemplate;
public JdbcVoucherRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
}
위와 같은 레포지토리를 테스트하기 위해 testContainers를 사용하여 외부 DB 컨테이너를 실행하야 합니다.
따라서 DataSource를 따로 설정해 줘야 하는데, 여기서 방식이 2가지로 나뉩니다.
- 코드를 통한 설정
- yaml을 통한 설정
[코드를 통한 설정]
코드의 경우 dataSource뿐만 아니라 container을 직접 실행 시켜줘야합니다.
MySQLContainer mySQLContainer = new MySQLContainer("mysql:8");
위와 같이 생성자에 dockerImageName이나 DockerImageName 클래스를 넣어주면 되는데
도커 허브에서 제공해주는 이미지 이름을 넣어주면 됩니다. 이로써 실행시키려 하는 도커 이미지를 찾게 됩니다.
그럼 어떻게 실행시켜 줄까? 자동으로 해줄까? 아쉽게도 위 코드까지만 작성하게 되면 이미지만 가지고 있지 실행하지는 않습니다. 따라서 실행시켜주기 위한 메서드가 있는데 `start()`와 `stop()`입니다.
여기서 그럼 생각해야 하는 것이 lifecycle이 어떻게 되고 따로 변경할 수 있나?입니다.
테스트 클래스는 기본 동작단위가 메서드인 만큼 testcontainer를 관리하는 lifecycle을 JUnit 프레임워크에 알려줘 같이 동작되도록 합니다. 따라서 클래스에 `@Testcontainers`를 적용시켜줘야 합니다.
또한 테스트가 실행될 때마다 testContainer가 먼저 실행되고 나중에 종료가 되어야 하기 때문에
@BeforeEach
void start() {
mySQLContainer.start();
}
@AfterEach
void end() {
mySQLContainer.stop();
}
위와 같이 작성할 수 있습니다.
하지만 이걸 계속 붙이기에는 불편하죠? 따라서 실행하고자 하는 컨테이너에 `@Container`를 붙이면 됩니다.
이제 연결이 될까요?
아직입니다. 지금까지는 실행하고자 하는 Container이고, 우리가 사용하려는 DB와의 연결을 위해 datasource의 설정 값들을 넣어 줘야 합니다.
테스트컨테이너는 기본적으로 username과 password, databaseName, port등 다양한 옵션을 제공해 줍니다.
public class MySQLContainer<SELF extends MySQLContainer<SELF>> extends JdbcDatabaseContainer<SELF> {
public static final String NAME = "mysql";
static final String DEFAULT_USER = "test";
static final String DEFAULT_PASSWORD = "test";
private static final String MYSQL_ROOT_USER = "root";
/***/
public MySQLContainer(DockerImageName dockerImageName) {
super(dockerImageName);
this.databaseName = "test";
this.username = "test";
this.password = "test";
dockerImageName.assertCompatibleWith(new DockerImageName[]{DEFAULT_IMAGE_NAME});
this.addExposedPort(MYSQL_PORT);
}
}
따라서 제공해주는 기본 값을 바로 넣어주면 됩니다.
어떻게 넣을 수 있을까요? 동적으로 넣어줘야 하기 때문에 @DynamicPropertySource 어노테이션을 붙여서 동적으로 적용할 수 있게 해줍니다.
@DynamicPropertySource
void registerPgProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url",mySQLContainer::getJdbcUrl);
registry.add("spring.datasource.username",mySQLContainer::getUsername);
registry.add("spring.datasource.password",mySQLContainer::getPassword);
}
이후 만약 schema.sql과 같이 초기화 해야하는 sql이 있다면 어떻게 해야할까요?
MySQLContainer mySQLContainer = (MySQLContainer) new MySQLContainer("mysql:8")
.withInitScript("schema.sql");
와 같이 `withInitScript("file")`로 컨테이너 실행시 같이 동작하게 하면 됩니다.
또한 리소스는 `resources/`가 초기 위치라고 생각하시면 됩니다. 다만, withInitScript의 경우 GenericContainer의 메서드로 MySQLContainer의 부모 클래스입니다. 따라서 다시 TypeCasting을 해주어야 사용가능합니다.
이렇게 하면 실행은 되게 됩니다. 테스트를 수행할 때마다 컨테이너를 실행시키기 때문에 테스트 소요 시간이 길어지게 됩니다.
따라서 이를 해결하기 위해, 필드변수를 클래스 변수로 변경하여 한번 생성할 때 동작되도록하게 합니다.
물론 장단점은 있습니다.
`장점`: 컨테이너를 한 번 생성하기 때문에 테스트 시간이 많이 짧아진다.
`단점`: 모든 docker를 통한 db를 사용하고 클래스 공통이다보니 모든 테스트에서 독립성을 가지지 못하기 때문에 순서를 강제하거나, @BeforeEach 등으로 매번 추가한 데이터를 삭제해 줘야 한다.
이렇다 보니 TDD를 중요시 한다면 과연 이러한 테스트를 매번할 수 있을까?에 대해서는 긍정적이지는 못하다고 생각합니다.
이래서 다른 포스트나 공식 문서에서도 `통합 테스트`를 강조한 이유 중 하나가 되지않을까 싶습니다.
[Yaml를 통한 설정]
yaml은 더 간단합니다.
spring.datasource에 바로 넣어주면 되는데, 이는 Testcontainer에서 application코드를 수정하지 않고 사용할 수 있는 `일회용 stand-in db`를 제공합니다.
따라서 아래와 같이 코드에서 처럼 MySQLConatiner를 시행시키지 않으려면 기본 테스트 데이터베이스 이름인 `test`를 사용해야 합니다.
spring:
datasource:
url: jdbc:tc:mysql:8://test
sql:
init:
mode: always
하지만 원하는 값을 넣고 싶다면 아래와 같이 추가 설정을 하면 됩니다.
@Container
static MySQLContainer mySQLContainer = (MySQLContainer) new MySQLContainer("mysql:8")
.withDatabaseName("custom");
----
# yaml
spring:
datasource:
url: jdbc:tc:mysql:8://custom
sql:
init:
mode: always
또한 sql을 초기화 하고 싶으면 sql:init:mode를 사용하여 설정하면 자동으로 schema.sql을 인식하여 적용하게 됩니다.
여기서 spring boot 2.3.0 이하 버전의 경우
`spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver`를 추가로 적용해 줘야 합니다.
관련 내용은 : JDBC support
JDBC support - Testcontainers for Java
JDBC support You can obtain a temporary database in one of two ways: Using a specially modified JDBC URL: after making a very simple modification to your system's JDBC URL string, Testcontainers will provide a disposable stand-in database that can be used
java.testcontainers.org
여기서 확인하시면 됩니다.
'잡다한것' 카테고리의 다른 글
DTO에 대한 생각 정리 (0) | 2023.07.09 |
---|