728x90
반응형
SMALL
이번에는 TestContainer라는 기술을 사용해서 Spring Boot에서 테스트 하는 방법을 알아보고자 한다.
이 포스팅은 아래 포스팅 이후의 포스팅이다. 그래서 아래 포스팅을 보지 않았다면 이 포스팅을 먼저 보고와야한다.
2024.10.02 - [Spring Advanced] - Redis를 사용해서 캐싱하기
TestContainer 써야 할까?
우선, 이 TestContainer가 유용한 경우는 이럴때다.
- DB 연동을 해야 하는 테스트를 해야할 때
- 테스트 환경에서 DB 연동은 해야겠는데 귀찮을 때
- DB를 새로 만들거나 구축하기는 정말 싫어서 테스트 후 바로 없애버리고 싶을 때
- Docker는 깔려있을 때
이런 경우 이 TestContainer가 장점을 발휘할 수 있다. 혹자는 그냥 H2로 테스트할때만 하면 되는거 아닌가? 싶을수 있다. MySQL과 H2는 같은 데이터베이스가 아니다. 즉, 동일한 작업을 해도 둘 중 하나는 에러가 발생하는데 둘 중 하나는 발생하지 않을 수 있다. 그래서 정확한 테스트라고 할 수는 없다.
또 다른 혹자는 그럼 그냥 Mocking하면 되는거 아닌가? 싶을수 있다. 이 또한, 사실 Mocking은 데이터베이스 연동 테스트가 전혀 아니다. 비즈니스 로직의 결함을 찾는것에 더 가까운 테스트이지 데이터베이스까지 연동한 테스트는 전혀 아니다.
TestContainer Dependencies
build.gradle
dependencies {
...
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
...
}
TestContainer Test Code
package cwchoiit.dmaker;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import cwchoiit.dmaker.dto.CreateDeveloperRequestDTO;
import cwchoiit.dmaker.dto.DeveloperDetailDTO;
import cwchoiit.dmaker.service.DMakerService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import static cwchoiit.dmaker.type.DeveloperLevel.SENIOR;
import static cwchoiit.dmaker.type.DeveloperType.BACK_END;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@Testcontainers
@SpringBootTest
class DMakerApplicationTests {
@Container
private static final GenericContainer<?> redisContainer =
new GenericContainer<>(DockerImageName.parse("redis:latest")).withExposedPorts(6379);
@Autowired
DMakerService dMakerService;
@BeforeAll
static void setUp() {
redisContainer.followOutput(new Slf4jLogConsumer(log));
}
@AfterAll
static void tearDown() {
redisContainer.close();
}
@DynamicPropertySource
private static void registerRedisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.port", () -> redisContainer.getMappedPort(6379).toString());
}
@Test
void contextLoads() throws IOException, InterruptedException {
CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Xayah")
.age(33)
.memberId("xayah")
.build();
dMakerService.createDeveloper(xayah);
dMakerService.getDeveloperByMemberId("xayah");
dMakerService.getDeveloperByMemberId("xayah");
GenericContainer.ExecResult execResult = redisContainer.execInContainer("redis-cli", "get", "developer:xayah");
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
DeveloperDetailDTO developerDetailDTO = mapper.readValue(execResult.getStdout(), DeveloperDetailDTO.class);
assertThat(redisContainer.isRunning()).isTrue();
assertThat(developerDetailDTO.getName()).isEqualTo("Xayah");
assertThat(developerDetailDTO.getAge()).isEqualTo(33);
assertThat(developerDetailDTO.getMemberId()).isEqualTo("xayah");
}
}
- 우선 클래스 레벨에 @Testcontainers 애노테이션을 붙여준다.
- 그리고 실제로 컨테이너를 생성해서 데이터베이스를 만들어야 하는데 나는 Redis를 사용해서 테스트해보기로 한다. 그래서 다음과 같이 필드 하나를 추가한다.
@Container
private static final GenericContainer<?> redisContainer =
new GenericContainer<>(DockerImageName.parse("redis:latest")).withExposedPorts(6379);
- @Container 애노테이션을 붙여서 이 필드가 컨테이너가 될 것임을 알려준다.
- 가장 최근의 이미지인 "redis:latest"를 사용하고, Redis의 기본 포트인 6379를 Expose한다.
@BeforeAll
static void setUp() {
redisContainer.followOutput(new Slf4jLogConsumer(log));
}
@AfterAll
static void tearDown() {
redisContainer.close();
}
- 이 부분은 둘 다 옵셔널하다. 나는 저 컨테이너에서 출력하는 로그 내용을 테스트 하면서 출력하고 싶어서 setUp()을 구현했고, tearDown()은 필요가 사실 없다. 왜냐하면 테스트가 끝나면 자동으로 컨테이너가 삭제된다.
@DynamicPropertySource
private static void registerRedisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.port", () -> redisContainer.getMappedPort(6379).toString());
}
- 중요한 부분은 이 부분이다. 컨테이너를 띄울때 6379와 매핑되는 포트는 임의로 지정된다. 같은 6379:6379면 좋갰지만 그게 아니다. 그래서 컨테이너가 띄워진 후 6379와 매핑된 포트를 스프링 부트의 설정값 중 spring.data.redis.port에 지정해줘야 한다. 그래야 정상적으로 통신이 가능해질테니.
- 그래서, 동적으로 속성값을 설정할 수 있는 방법을 이렇게 @DynamicPropertySource 애노테이션으로 제공한다.
@Test
void contextLoads() throws IOException, InterruptedException {
CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Xayah")
.age(33)
.memberId("xayah")
.build();
dMakerService.createDeveloper(xayah);
dMakerService.getDeveloperByMemberId("xayah");
dMakerService.getDeveloperByMemberId("xayah");
GenericContainer.ExecResult execResult = redisContainer.execInContainer("redis-cli", "get", "developer:xayah");
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
DeveloperDetailDTO developerDetailDTO = mapper.readValue(execResult.getStdout(), DeveloperDetailDTO.class);
assertThat(redisContainer.isRunning()).isTrue();
assertThat(developerDetailDTO.getName()).isEqualTo("Xayah");
assertThat(developerDetailDTO.getAge()).isEqualTo(33);
assertThat(developerDetailDTO.getMemberId()).isEqualTo("xayah");
}
- 이 부분은 실제 테스트 코드다. 사실 이 부분은 별 게 없다. 나의 경우 getDeveloperByMemberId()는 Redis로 캐시할 수 있게 구현했다. 그래서 저 메서드를 두번 호출헀을 땐, 첫번째는 redis에 저장된 값이 없기 때문에 실제 데이터베이스에서 값을 가져올 것이고, 두번째 호출했을 땐, redis에 저장된 값이 있기 때문에 바로 캐싱이 가능해진다.
- 그리고, redis에 저장됐기 때문에, redisContainer.execInContainer("redis-cli", "get", "developer:xayah")를 호출하면 저장된 캐시의 value가 반환될 것이다.
- 그 반환된 값을 JSON으로 역직렬화를 하여 객체가 가진 값들을 비교한다.
어떻게 JSON으로 직렬화해서 저장이 바로 됐나요? → 이전 포스팅을 참고해야 한다. RedisConfig 클래스로 설정을 했다. 그리고 @SpringBootTest이므로 스프링 컨테이너가 온전히 띄워지기 때문에 설정값이 적용된 상태에서 이 테스트가 진행되는 것이다.
테스트 결과는 다음과 같이 성공이다.
참고. 컨테이너가 띄워지고 삭제되는 것을 확인
저 테스트가 실행되면, 컨테이너가 실제로 띄워진다. 그리고 테스트가 끝나면 컨테이너가 자동으로 삭제된다. 확인해보면 좋을것이다.
728x90
반응형
LIST
'Spring Advanced' 카테고리의 다른 글
Redis를 사용해서 캐싱하기 (0) | 2024.10.02 |
---|---|
Mockito를 사용한 스프링 프로젝트 단위 테스트 (4) | 2024.09.29 |
AOP (Part. 3) - 포인트컷 (0) | 2024.01.02 |
AOP (Part.2) (0) | 2024.01.02 |
AOP(Aspect Oriented Programming) (0) | 2023.12.29 |