이전 포스트에서 WebMvcTest, Mock Test로 Controller와 Service만 슬라이스 테스트를 구성했습니다.
운영환경의 DB와 동일한 DB로 테스트를 추가하여 더욱 견고한 웹 애플리케이션을 유지보수 하고싶습니다.
TestContainers를 통해 docker container로 DB를 올려서 중간 범위 테스트^1 를 구성할 수 있습니다.
docker compose 파일과 test container로 테스트 환경 구축하기
개발환경 구축시 단일 컨테이너들을 각각 관리하는 것보다 docker compose 파일하나를 구성하여 로컬 개발환경에 필요한 스택을 모아서 구축하는 경우가 흔합니다
TestContainers는 로컬환경의 docker comose 파일을 사용할 수 있어서 docker compose로 TestContainers를 통해 테스트 환경을 구축할 수 있습니다^2
ApplicationContext
TestContainers를 통해 docker-compose로 구성한 도커 컨테이너들을 SpringFramework Test와 연동 가능합니다.
MySQL DB를 도커 컨테이너로 띄운후에 테스트를 진행해야한다면, 도커 컨테이너를 띄운후에 SpringFramework에서 컨테이너를 접근해야합니다.
ApplicationContext는 스프링 애플리케이션의 핵심 요소이며, Bean의 라이프사이클 관리 및 다양한 기능을 다룹니다.
ApplicationContextInitializer를 구현해서 초기화될때 Spring 애플리케이션에서 컨테이너의 정보를 참조하도록 구성할 수 있습니다.
Spring 애플리케이션에서는 프로퍼티 객체를 통해 환경변수를 활용할 수가 있습니다.^SpringBoot_Properties
ApplicationContextInitializer를 구현할때 TestContainers의 컨테이너 정보를 접근해서 port나 host 정보를 참조하여 테스트 프로퍼티 객체를 업데이트할 수 있습니다.
코드 예시
static {
rdbms = new DockerComposeContainer(new File("infra/test/docker-compose.yml"))
.withExposedService(
"local-db",
3306,
Wait.forLogMessage(".*ready for connections.*", 1)
.withStartupTimeout(Duration.ofSeconds(300))
)
.withExposedService(
"local-db-migrate",
0,
Wait.forLogMessage("(.*Successfully applied.*)|(.*Successfully validated.*)", 1)
.withStartupTimeout(Duration.ofSeconds(300))
);
rdbms.start();
}
static class IntegrationTestInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
Map<String, String> properties = new HashMap<>();
var rdbmsHost = rdbms.getServiceHost("local-db", 3306);
var rdbmsPort = rdbms.getServicePort("local-db", 3306);
properties.put("spring.datasource.url", "jdbc:mysql://" + rdbmsHost + ":" + rdbmsPort + "/study");
TestPropertyValues.of(properties)
.applyTo(applicationContext);
}
}
마이그레이션
flyway를 통해 migration을 관리한다면 TestContainers에 flyway container를 올려서 마이그레이션을 실행한후에 테스트를 진행하도록 구성할 수 있습니다.
test 데이터 롤백
@SpringBootTest에 @Transactional을 적용하지 않은 이유^3를 개발바닥 향로님의 테스트 데이터 초기화 포스트를 보면서 공감하는 부분이 있어서 @Transactional 애노테이션을 제거했습니다.
test 데이터 롤백을 자유롭게 할수 있는 팁
향로님이 올려주신 테스트 데이터 초기화하는 객체를 만들어서 재활용 할 수 있습니다
데이터 초기화 코드 예시
@RequiredArgsConstructor
@Component
public class CleanUp {
private final JdbcTemplate jdbcTemplate;
private final EntityManager entityManager;
@Transactional
public void all() {
List<String> tables = entityManager.getMetamodel().getEntities().stream()
.map(this::getTableName)
.collect(Collectors.toList());
tables.forEach(table -> jdbcTemplate.execute("TRUNCATE TABLE " + table));
}
private String getTableName(EntityType<?> entityType) {
Table tableAnnotation = entityType.getJavaType().getAnnotation(Table.class);
return (tableAnnotation != null && !tableAnnotation.name().isEmpty())
? tableAnnotation.name()
: entityType.getName();
}
}
테스트 데이터를 초기화할때 전제로 두가지가 만족해야합니다.
- FK을 사용하지 않습니다.
- truncate 명령으로 데이터를 초기화할때 FK 제약으로 초기화가 어렵습니다
- Table name 메타데이터를 구성해놓습니다.
- entityManager로 entity들의 메타정보에서 table 정보를 얻어서 쿼리를 실행합니다
저같은 경우에는 추가로 관계컬럼들을 nullable하도록 수정했습니다.
EntityManager에서 아직 영속화되지 않은 데이터에 대해서 save할때 TransientObjectException^4 같은 익셉션이 발생하기 때문에 nullable하도록 바꾸었습니다.
mysql은 FK를 걸어두면 index가 자동으로 생성되기 때문에 관계컬럼에 대해서 index를 구성하지 않았으나, FK를 모두 사용하지 않았기 때문에 index를 따로 구성했습니다.
'스프링 프레임워크 > test' 카테고리의 다른 글
Springboot test - 2 - Service test with mockito (0) | 2024.08.07 |
---|---|
Springboot test - 1 - controller test (0) | 2024.08.07 |