728x90
반응형
SMALL

이번에는 TestContainer라는 기술을 사용해서 Spring Boot에서 테스트 하는 방법을 알아보고자 한다. 

이 포스팅은 아래 포스팅 이후의 포스팅이다. 그래서 아래 포스팅을 보지 않았다면 이 포스팅을 먼저 보고와야한다.

2024.10.02 - [Spring Advanced] - Redis를 사용해서 캐싱하기

 

Redis를 사용해서 캐싱하기

Spring Boot + Redis는 환상의 조합이다. 정말 많은 개발자들이 사용하는 조합이고, Redis는 대규모 트래픽을 처리하는 서비스에서는 거의 필수요소라고 볼 수 있다. 왜냐? 아주 빠르고 캐싱이 가능하

cwchoiit.tistory.com

 

TestContainer 써야 할까?

우선, 이 TestContainer가 유용한 경우는 이럴때다.

  • DB 연동을 해야 하는 테스트를 해야할 때
  • 테스트 환경에서 DB 연동은 해야겠는데 귀찮을 때
  • DB를 새로 만들거나 구축하기는 정말 싫어서 테스트 후 바로 없애버리고 싶을 때
  • Docker는 깔려있을 때

이런 경우 이 TestContainer가 장점을 발휘할 수 있다. 혹자는 그냥 H2로 테스트할때만 하면 되는거 아닌가? 싶을수 있다. MySQLH2는 같은 데이터베이스가 아니다. 즉, 동일한 작업을 해도 둘 중 하나는 에러가 발생하는데 둘 중 하나는 발생하지 않을 수 있다. 그래서 정확한 테스트라고 할 수는 없다.

 

또 다른 혹자는 그럼 그냥 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

+ Recent posts