들어가며
요즘카페 서비스는 현재 TEST환경에서 DB MySql을 사용하고 있다.
이 과정에서 여러 문제점이 있었다.
- 테스트가 작동하는 모든 환경에 MySql을 설치하여야 한다.
- Teamcity CICD 빌드 과정에서 Docker In Docker로 MySql을 띄워야한다.
이런 불편함 때문에 관리 포인트가 적은 TestContainer를 사용하게 되었다.
TestContainer
테스트 컨테이너 라는 것은 자바 코드를 통해서 도커 컨테이너를 제어하여 테스트를 도와주는 라이브러리이다.
이를 통해 테스트에 필요한 다른 의존성들을 제어할 수 있다.
생각보다 공식문서 자료가 잘 되어있다.
테스트컨테이너란 코드로 도커 컨테이너를 제어하여 통합테스트를 도와주는 라이브러리이다.
로컬에 설치된 도커데몬과 연동되어 테스트코드가 실행되기 전 테스트를 위한 일회성 컨테이너를 생성하고 테스트 수행 후 컨테이너를 삭제하여 편의성을 높여준다.
TestContainers 로 테스트 환경 구축하기
testImplementation "org.testcontainers:junit-jupiter:1.19.0"
testImplementation 'org.testcontainers:mysql'
org.testcontainers:junit-jupiter를 통해 junit을 지원합니다. 또한 MySql 컨테이너를 사용하기 때문에 org.testcontainers:mysql 를 설정해주었다.
@Autowired
protected static MySQLContainer container;
static {
container = (MySQLContainer) new MySQLContainer("mysql:8.0")
.withDatabaseName("yozm-cafe")
.withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD);
container.start();
}
@DynamicPropertySource
static void configureProperties(final DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", container::getJdbcUrl);
registry.add("spring.datasource.username", () -> ROOT);
registry.add("spring.datasource.password", () -> ROOT_PASSWORD);
}
static으로 MySqlContainer와 함께 설정 파일을 삽입해주고 시작하였다. 이렇게 해준 이유는 컨테이너를 매번 실행하고 종료 시킬 경우에는 테스트 속도가 엄청 느려지기 때문에 정적으로 선언을 해놓고 사용을 하기 위함이다.
@DynamicPropertySource application.yaml파일 등에 설정해둔 환경 설정을 동적으로 덮어씌우기 위해서 사용하였다.
다음과 같이 테스트 컨테이너가 잘 뜨는 것을 확인할 수 있다.
이렇게 함으로써 모든 테스트가 진행되는 동안 한번의 컨테이너만 생긴다. 또한 테스트마다 컨테이너가 독립적으로 수행되며 멱등성을 보장하고 운영환경과 거의 동일한 환경에서 통합테스트를 수행하여 보다 더 정확한 테스트를 할 수 있을 것 같다.
테스트 격리
기존에 요즘카페 서비스는 Truncate 문을 사용하여 테스트 DB를 초기화 시켜주었다.
이러한 부분이 너무 불편하고 Table이 늘어남에 따라 관리해줘야하는 요소가 많아지고 놓칠 수 있어서 코드로 처리해보기로 하였다.
@Component
public class DbCleaner {
private static final int OFF = 0;
private static final int ON = 0;
private static final String FOREIGN_KEY_CHECKS = "SET FOREIGN_KEY_CHECKS = ";
private static final String TRUNCATE_TABLE = "TRUNCATE TABLE ";
@PersistenceContext
private EntityManager em;
@Transactional
public void dbClear() {
setForeignKeyCheck(OFF);
truncateAllTable();
setForeignKeyCheck(ON);
}
private void setForeignKeyCheck(final int enabled) {
em.createNativeQuery(FOREIGN_KEY_CHECKS + enabled).executeUpdate();
}
private void truncateAllTable() {
final List<String> dbTableName = findDbTableName();
for (final String tableName : dbTableName) {
em.createNativeQuery(TRUNCATE_TABLE + tableName).executeUpdate();
}
}
private List<String> findDbTableName() {
return em.createNativeQuery("SHOW TABLES").getResultList();
}
}
다음과 같은 코드를 작성하였다.
setForeignKeyCheck() 는 truncate를 위해 모든 외래키를 해제하고 다시 등록시킨다.
truncateAllTable() 은 모든 테이블을 Truncate 한다. EntityManger를 통해 테이블의 명을 가져오고 이 테이블명을 토대로 Truncate 문을 실행하는 것이다.
public class AfterEachDbCleaner implements AfterEachCallback {
@Override
public void afterEach(ExtensionContext context) throws Exception {
final ApplicationContext applicationContext = SpringExtension.getApplicationContext(context);
final DbCleaner dbCleaner = applicationContext.getBean(DbCleaner.class);
dbCleaner.dbClear();
}
}
위의 작성한 dbClear() 는 테스트가 실행된 이후 매번 실행되기를 원했다.
그렇기 때문에 AfterEachCallback implements한 AfterEachDbCleaner 를 만들어 실행되도록 하였다.
이렇게 처리하면서 db 테이블이 바뀜에도 동적으로 Truncate를 처리할 수 있고 테스트 격리를 수행할 수 있게 되었다.
'코코코딩공부' 카테고리의 다른 글
필터를 사용해 API 성능 로그 만들기 (2) | 2023.10.31 |
---|---|
테스트를 더 빠르게 진행시켜보자 (0) | 2023.10.21 |
DB Replication (1) | 2023.10.15 |
DataSource 라우팅이 안되는 이유. OSIV (1) | 2023.10.09 |
Flyway 적용해보기 (0) | 2023.09.23 |