왜 Testcontainer를 사용하는가?
Spring DB 테스트 Rollback 원리
Spring에서는 테스트 코드 작성을 위해 @Test 어노테이션과 @Transactional 어노테이션을 함께 사용하면 기본적으로 롤백이 활성화됩니다. 이는 테스트가 실행된 후 변경된 데이터가 실제 데이터베이스에 영향을 미치지 않도록 보장하는 것입니다.
NoSql 테스트
NoSQL 데이터베이스를 사용할 때는 이러한 접근 방식이 다소 복잡해질 수 있습니다.
NoSQL 데이터베이스는 일반적으로 ACID 트랜잭션을 지원하지 않거나, 관계형 데이터베이스와는 다르게 구현됩니다. 따라서 Spring의 트랜잭션 처리 메커니즘을 NoSQL 데이터베이스에 적용하는 것은 번거롭습니다.
NoSQL 데이터베이스를 사용하는 경우에는 다음과 같은 테스트 전략을 고려할 수 있습니다:
- Testcontainer 사용: Testcontainer를 활용하여 테스트용 DB 컨테이너를 실행합니다. 기존 DB와 다른 컨테이너를 제공하기때문에 실제 데이터에 영향이 없습니다.
- 래플리카 또는 클러스터 구성: 복제본과 분산환경에서 테스트를 진행하기에 실제 데이터에 영향이 없습니다.
- 인메모리 데이터베이스 사용: 테스트 용도로 인메모리 데이터베이스를 사용하여 테스트를 진행할 수 있습니다. 이를 통해 실제 데이터베이스 서버 없이도 테스트를 수행할 수 있으며, 속도와 효율성을 높일 수 있습니다.
제가 진행할려는 방식은 1번 Testcontainer를 이용하여 Elasticsearch Test를 스프링 부트에서 진행할려고 합니다.
실습전 이론과 진행 방식 요약
이론
- DataElasticsearchTest 어노테이션 적용, 기존 @SpringBootTest+ElasticsearchTest 으로 이해하면됩니다
- ServiceConnection 어노테이션
- 스프링부트 3.1 버전부터는 @DynamicPropertySource 대신 @ServiceConnection 어노테이션이 도입되었습니다.
- @ServiceConnection은 테스트 컨테이너 인스턴스 필드에 적용되어 외부 서비스와의 연결 정보를 설정할 수 있습니다.
- 이를 통해 기존의 복잡한 설정 변경 없이도 외부 서비스와 연결할 수 있습니다.
- @DynamicPropertySource
- @DynamicPropertySource는 스프링 프레임워크 5.2.5 버전부터 도입된 메서드 레벨 어노테이션입니다.
- 이 어노테이션은 테스트 시 외부 컴포넌트(데이터베이스, 메시징 시스템 등)와 연결되는 애플리케이션을 테스트하는 데 사용됩니다.
- 기존에는 외부 컴포넌트와 연결하기 위해 복잡한 설정 변경이 필요했지만, @DynamicPropertySource를 사용하면 동적으로 속성을 추가할 수 있어 편리합니다.
진행과정
- DataElasticsearchTest 어노테이션 등록: Spring Test에서 Elasticsearch를 사용하는 테스트를 위해 @DataElasticsearchTest 어노테이션을 등록합니다. 이를 통해 Elasticsearch와 관련된 테스트가 가능하게 됩니다.
- ElasticsearchContainer를 통한 컨테이너 등록: @Container 어노테이션을 사용하여 Testcontainer에서 제공하는 ElasticsearchContainer를 등록합니다. 이를 통해 테스트를 실행할 때 특정 버전의 Elasticsearch를 컨테이너로 실행할 수 있습니다.
- ServiceConnection을 통한 컨테이너 실행 설정: @ServiceConnection 어노테이션을 사용하여 실행된 Elasticsearch과의 config작업을 진행합니다.
- 테스트 셋업 설정: @BeforeEach 어노테이션을 사용하여 테스트 메서드 실행 전에 설정을 초기화합니다. 여기서는 Testcontainer가 실행 중인지 확인하고, 실행되지 않았다면 기다린 후 실행되도록 설정합니다. 이를 통해 Docker가 해당 컨테이너 이미지를 실행하고 데이터를 테스트할 수 있도록 준비합니다.
실습
당연한 이야기지만 Docker가 실행중이어야 Testcontainer가 작동합니다.
예제 코드
@DataElasticsearchTest
@Testcontainers
@Slf4j
public class ElasticsearchIntegrationTests {
@Container
@ServiceConnection
public static ElasticsearchContainer ES_CONTAINER = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.17.5");
@Autowired
private ProductRepository productRepository;
@Test
void testDatabaseIsRunning() {
assertThat(ES_CONTAINER.isRunning()).isTrue();
}
@Test
public void testProductRepository() {
var product = productRepository.save(new Product(null, "test", BigDecimal.ONE));
assertThat(product).isNotNull();
assertThat(product.id()).isNotNull();
productRepository.findById(product.id()).ifPresent(
p -> {
log.debug("found product by id: {}", p);
assertThat(p.name()).isEqualTo("test");
}
);
}
}
예제 코드가 잘 작동했다면 이제 실제 코드로 가봅니다.
실제 사용 버전은 Springboot 3.2, elasticsearch 8.*이지만 테스트 코드는 7.*버전에서 진행했습니다. 이유는 차차 설명하겠습니다.
실제 코드
- ElasticsearchRepository를 구현한 실제 Repository를 Autowired해옵니다.
public interface QuestionsRepository extends ElasticsearchRepository<QuestionEntity, String> - 저의 경우 복잡한 쿼리 사용을 위해 BoolQueryBuilder 빈 등록과 데이터 조회 등록을 위해 ElasticsearchTemplate를 사용했으므로 각각 MockBean, Autowired시켜줍니다.
- Testcontainer 이미지 작동 후에 테스트 진행하도록 BeforeEach()에 await메서드를 통해 대기하도록 합니다. 추가로 테스트에 사용할 더미데이터를 하나 삽입해줍니다.
@DataElasticsearchTest
@Tag("db_test")
@Testcontainers
public class QuestionsRepositoryTest {
// 8.x SSL HTTPS ERROR -> 7.x HTTP
private final static String IMAGE_NAME = "docker.elastic.co/elasticsearch/elasticsearch:7.17.5";
//private final static String IMAGE_NAME = "docker.elastic.co/elasticsearch/elasticsearch:8.11.3";
@Container
@ServiceConnection
public static ElasticsearchContainer ES_CONTAINER = new ElasticsearchContainer(IMAGE_NAME);
@Autowired
private QuestionsRepository questionsRepository;
@MockBean
private BoolQueryBuilder boolQueryBuilder;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
private final Long existExamId = 0L;
private final String questionUuid = "questionsuuid";
@BeforeEach
public void setup() {
await()
.untilAsserted(() -> {
assertThat(ES_CONTAINER.isRunning()).isTrue();
});
QuestionEntity question = QuestionEntity.builder()
.id(questionUuid)
.examId(existExamId)
.type("객관식")
.question("다음 중 총중량 1.5톤 피견인 승용자동차를 4.5톤 화물자동차로 견인하는 경우 필요한 운전면허에 해당하지 않은 것은?")
.options(List.of(
"① 제1종 대형면허 및 소형견인차면허",
"② 제1종 보통면허 및 대형견인차면허",
"③ 제1종 보통면허 및 소형견인차면허",
"④ 제2종 보통면허 및 대형견인차면허"))
.questionImagesIn(List.of(
ImageDto.builder().url("url1").description("설명1").attribute("속성1").build()))
.questionImagesOut(List.of(
ImageDto.builder().url("url2").description("설명2").attribute("속성2").build()))
.answers(List.of(4))
.commentary("도로교통법 시행규칙 별표18 총중량 750킬로그램을 초과하는 3톤 이하의 피견인 자동차를 견인하기 위해서는 견인 하는 자동차를 운전할 수 있는 면허와 소형견인차면허 또는 대형견인차면허를 가지고 있어야 한다.")
.commentaryImagesIn(List.of(
ImageDto.builder().url("url3").description("설명3").attribute("속성3").build()))
.commentaryImagesOut(List.of(
ImageDto.builder().url("url4").description("설명4").attribute("속성4").build()))
.tagsMap(Map.of("category", List.of("화물")))
.build();
questionsRepository.save(question);
}
}
테스트 결과
해결 못한 부분
성공적인 Elasticsearch 7.x 테스트, 실패한 8.x 테스트
https://github.com/spring-projects/spring-boot/issues/35926
Improve Elasticsearch ServiceConnection · Issue #35926 · spring-projects/spring-boot
Currently, ElasticsearchContainerConnectionDetailsFactory only maps nodes and that works with Elasticsearch 7. However, Elasticsearch 8 has security enabled by default and additional configuration ...
github.com
- 대략적인 내용: Elasticsearch 8.x버전은 기본적으로 SSL이 활성화 되어있어 HTTPS로 테스트 되어야한다. 스프링은 기본적으로 SSL비활성을 전제로 HTTP로 테스트한다.
- 결론: 별도로 증명을 할 수 있는 env파일 또는 이외의 방법을 생각해야하는데 이는 테스트의 범위를 넘어간다고 생각한다 → 따라서 7.x버전으로 테스트 진행
비교적 최신 내용이라 잘못된 내용이 많을 수 있습니다. 참고 바랍니다
참고
SpringBoot 3.1이상에서 Testcontainer 사용법
https://www.baeldung.com/spring-boot-built-in-testcontainers
- @ServiceConnetion을 사용하여 SpringBoot의 자동 구성이 필요한 모든 속성을 동적으로 등록가능, 무대 뒤에서 컨테이너 클래스 또는 도커 이미지 이름을 기반으로 필요한 속성을 결정함
- 3.1이전 버전: @DynamicPropertySource 주석을 이용해서 속성을 추가했어야함
Core Features
In the absence of an Executor bean in the context, Spring Boot auto-configures an AsyncTaskExecutor. When virtual threads are enabled (using Java 21+ and spring.threads.virtual.enabled set to true) this will be a SimpleAsyncTaskExecutor that uses virtual t
docs.spring.io
- 각 컨테이너 연결 방식
https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.nosql.elasticsearch
Data
The Spring Framework provides extensive support for working with SQL databases, from direct JDBC access using JdbcClient or JdbcTemplate to complete “object relational mapping” technologies such as Hibernate. Spring Data provides an additional level of
docs.spring.io
- ElasticSearch+Spring
'스프링 > 테스트' 카테고리의 다른 글
Postman을 활용한 API 테스트에서의 로그인 세션 유지 및 타 API 테스트 (0) | 2024.04.19 |
---|---|
[스프링] JUnit @Tag를 이용한 테스트 코드 분리 (0) | 2024.03.15 |