본문 바로가기
Spring/SpringBoot

[SpringBoot] Redis 테스트 환경 구축하기 (2) - Test Container

by J4J 2024. 7. 27.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 redis 테스트 환경 구축하기 마지막인 test container에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

관련 글

 

[SpringBoot] Redis 사용하기 (1) - Redis란 무엇인가?

[SpringBoot] Redis 사용하기 (2) - Redis Repository 사용하기

[SpringBoot] Redis 사용하기 (3) - Redis Template 사용하기

[SpringBoot] Redis 사용하기 (4) - Redis Cache Manager 사용하기

 

 

반응형

 

 

이전 글

 

[SpringBoot] Redis 테스트 환경 구축하기 (1) - Embedded

 

 

 

 

Test Container란 ?

 

test container는 docker 컨테이너 기반의 독립된 환경에서 테스트를 도와주는 오픈 소스입니다.

 

test container의 동작 원리는 다음과 같습니다.

 

  • 테스트를 수행하기 전 테스트 동작이 필요한 환경을 컨테이너 기반으로 구축
  • 테스트를 수행할 때 생성된 컨테이너에 접근하여 테스트
  • 테스트가 어떤 이유로든 종료되면 생성된 컨테이너는 제거

 

 

 

test container는 위와 같은 동작 원리를 가지고 있기 때문에 다음과 같은 장점을 제공해 줍니다.

 

  • 각 테스트만을 위한 격리된 환경을 제공하기 때문에 테스트 간 발생될 수 있는 간섭을 제거하여 신뢰성 향상
  • 로컬에서 테스트, CI/CD 파이프라인 테스트 등과 같이 어떤 환경에서든 동일한 테스트가 동작될 수 있도록 보장
  • 한 번 설정한 컨테이너 환경은 다양한 곳에서 재 사용되어 활용되어 테스트를 위한 추가 설정이 필요 없음
  • 컨테이너에서 제공해 주는 이미지를 활용하여 DB / Message 처리 등의 환경을 모두 테스트할 수 있음

 

 

 

 

[SpringBoot] @DataJpaTest In-Memory DB를 활용하여 테스트하기를 확인해 보면 in-memory DB를 활용하여 테스트에 대한 효율성과 신뢰도를 높이는 방식을 볼 수 있습니다.

 

하지만 in-memory DB의 단점은 실제 서버에서 사용되는 DB datasource는 mysql / postgresql 등을 사용하는데 테스트에서는 h2를 사용하게 됩니다.

 

즉, 테스트에서 동작되는 환경과 애플리케이션이 실제 구동되는 환경의 datasource가 서로 다르기 때문에 예상하지 못한 문제 발생 가능성이 있습니다.

 

 

 

하지만 test container는 위와 같은 장점을 가지고 있기 때문에 in-memory DB를 사용했을 때 발생되는 문제를 해소할 수 있습니다.

 

h2를 사용하는 대신 애플리케이션에서 실제 동작되는 datasource에 맞는 환경으로 test container를 구축할 수 있고 테스트를 수행할 때 애플리케이션 환경과 동일한 설정으로 동작할 수 있게 도와줍니다.

 

 

 

이처럼 test container는 많은 장점을 가지고 있지만 다른 기술들처럼 다음과 같은 단점도 가지고 있습니다.

 

  • test container가 동작되는 곳에서는 docker 환경이 구축되어 있어야 함
  • container 생성을 위한 추가 리소스가 필요함
  • container 내부로 접근하는 것은 host 자체에 접근하는 것보다 성능적으로 좋지 않음

 

 

 

결국 항상 test container가 정답은 아니라고 할 수 있습니다.

 

그래서 프로젝트 및 서비스를 운영하는 각 환경을 고려했을 때 가장 적합한 방식을 선택하면 되고, 큰 이슈가 없다면 개인적으로는 독립적인 환경과 실제 애플리케이션의 동작 환경을 보장하기 위한 test container의 사용을 권장합니다.

 

 

 

 

Test Container 설정 전 테스트

 

test container를 설정하기 전 다음과 같이 간단히 테스트 코드를 작성해 보겠습니다.

 

// user entity
package com.jforj.redistestcontainer.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String id;
    private String name;
    private int age;
}


// user repository
package com.jforj.redistestcontainer.repository;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.redistestcontainer.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;

    public void createUser(String id, String name, int age) {
        User user = User
                .builder()
                .id(id)
                .name(name)
                .age(age)
                .build();

        try {
            redisTemplate.opsForValue().set(id, objectMapper.writeValueAsString(user));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public User getUser(String id) {
        try {
            return objectMapper.readValue(redisTemplate.opsForValue().get(id), User.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}


// user repository test
package com.jforj.redistestcontainer.repository;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.redistestcontainer.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Repository;

import static org.assertj.core.api.Assertions.assertThat;

@DataRedisTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
@Import(ObjectMapper.class)
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void getUserTest() {
        // given
        String id = "j4j";
        String name = "jforj";
        int age = 123;

        userRepository.createUser(id, name, age);

        // when
        User user = userRepository.getUser(id);

        // then
        assertThat(user).isNotNull();
        assertThat(user.getName()).isEqualTo(name);
    }
}

 

 

 

redis를 사용할 수 있는 환경이 구성되어 있는 상태에서 위와 같이 코드를 작성한다면 다음과 같이 테스트가 동작되는 것을 확인할 수 있습니다.

 

테스트 동작 결과

 

 

 

그러나 위의 테스트는 실제 redis 서버에 연결되어 테스트가 동작되고 있습니다.

 

그래서 테스트를 위해 어떤 환경에서든 redis 서버와 연결이 필요하며 다음과 같이 테스트 결과가 redis 서버에 영향을 주게 됩니다.

 

테스트 동작 후 redis 서버 현황

 

 

 

 

Test Container 설정 방법

 

[ 1. dependency 추가 ]

 

dependencies {
    // test container
    testImplementation 'org.testcontainers:testcontainers:1.19.8'
    testImplementation 'org.testcontainers:junit-jupiter:1.19.8'
}

 

 

 

[ 2. test container 설정 추가 ]

 

redis가 동작할 수 있는 test container 설정을 위해 다음과 같이 클래스 파일을 /test 하위에 배치하면 됩니다.

 

package com.jforj.redistestcontainer.config;

import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;

public class RedisTestContainerConfig implements BeforeAllCallback {
    private static final String REDIS_IMAGE = "redis:7.0.8-alpine";
    private static final int REDIS_PORT = 6379;
    private GenericContainer redisGenericContainer;

    @Override
    public void beforeAll(ExtensionContext context) {
        redisGenericContainer =
                new GenericContainer(DockerImageName.parse(REDIS_IMAGE))
                        .withExposedPorts(REDIS_PORT);

        redisGenericContainer.start();

        System.setProperty("spring.data.redis.host", redisGenericContainer.getHost());
        System.setProperty("spring.data.redis.port", String.valueOf(redisGenericContainer.getMappedPort(REDIS_PORT)));
    }
}

 

 

 

 

[ 3. test container 설정 테스트에 추가 ]

 

test container 설정을 했다면 테스트 동작되는 클래스에 extend 처리를 다음과 같이 해주시면 됩니다.

 

package com.jforj.redistestcontainer.repository;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.redistestcontainer.config.RedisTestContainerConfig;
import com.jforj.redistestcontainer.entity.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Repository;

import static org.assertj.core.api.Assertions.assertThat;

@DataRedisTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
@Import(ObjectMapper.class)
@ExtendWith(RedisTestContainerConfig.class) // redis test container 클래스 추가
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void getUserTest() {
        // given
        String id = "j4j";
        String name = "jforj";
        int age = 123;

        userRepository.createUser(id, name, age);

        // when
        User user = userRepository.getUser(id);

        // then
        assertThat(user).isNotNull();
        assertThat(user.getName()).isEqualTo(name);
    }
}

 

 

 

[ 4. 테스트 ]

 

위와 같이 모든 설정을 완료했다면 테스트를 해보겠습니다.

 

test container는 위에서 얘기한 대로 container 동작을 위해 테스트가 실행되는 환경에는 docker 사용이 가능해야 합니다.

 

먼저, docker가 없는 곳에서 테스트를 수행하면 다음과 같은 결과를 확인할 수 있습니다.

 

docker 없는 테스트 결과

 

 

 

이번엔 테스트가 실행되는 환경에 docker를 사용할 수 있도록 세팅해 보겠습니다.

 

개인 PC에서 테스트를 해보는 경우라면 docker desktop을 설치하여 사용해 볼 수 있습니다.

 

저도 동일하게 docker desktop을 실행한 뒤 다시 테스트를 돌려보면 다음과 같은 결과를 확인할 수 있으며 test container에서 테스트가 되었기에 redis 서버에는 영향을 주지 않고 독립적으로 동작하는 것을 볼 수 있습니다.

 

docker 있는 테스트 결과

 

 

 

 

GitLab 파이프라인 전/후 비교

 

위에서 얘기한 대로 test container를 사용한다면 개인 PC와 같은 로컬에서 테스트하는 것과 CI/CD 파이프라인 등을 통해 동작되는 환경에서 테스트하는 것이 동일한 결과를 제공해 줄 수 있습니다.

 

이를 확인하기 위해 gitlab을 이용하여 파이프라인을 적용해 보겠습니다.

 

 

 

파이프라인 동작을 위해 다음과 같이 .gitlab-ci.yml 설정을 해보겠습니다.

 

// .gitlab-ci.yml
stages:
  - build
  - test

build:
  image: gradle:8.8.0-jdk17
  stage: build
  script:
    - chmod +x gradlew
    - ./gradlew bootJar
  artifacts:
    paths:
      - build/libs/*.jar
    expire_in: 1 days

test:
  image: gradle:8.8.0-jdk17
  stage: test
  script:
    - chmod +x gradlew
    - ./gradlew test

 

 

 

그런 뒤 test container를 사용하지 않는 환경으로 설정하고 연결되어 있는 gitlab repository에 push를 하면 파이프라인이 동작되지만 gitlab에서 redis 서버에 연결될 수 없기 때문에 파이프라인 동작이 실패하는 것을 볼 수 있습니다.

 

test container 설정 전 배포 파이프라인

 

 

 

 

다음으로 다시 test  container를 사용할 수 있는 환경으로 변경한 뒤 push를 해보겠습니다.

 

test container 설정 클래스는 위와 동일하게 설정하고 gitlab에서 docker 사용 환경을 설정하기 위해 .gitlab-ci.yml 파일을 다음과 같이 추가 설정해주셔야 합니다.

 

// .gitlab-ci.yml
stages:
  - build
  - test

services:
  - docker:dind
variables:
  DOCKER_HOST: tcp://docker:2375

build:
  image: gradle:8.8.0-jdk17
  stage: build
  script:
    - chmod +x gradlew
    - ./gradlew bootJar
  artifacts:
    paths:
      - build/libs/*.jar
    expire_in: 1 days

test:
  image: gradle:8.8.0-jdk17
  stage: test
  script:
    - chmod +x gradlew
    - ./gradlew test

 

 

 

그리고 push를 하게 되면 test container가 올바르게 동작되고, redis 서버에 연결하는 것 없이도 테스트가 성공하는 것을 확인할 수 있습니다.

 

test container 설정 후 배포 파이프라인

 

test container 설정 후 배포 파이프라인

 

 

 

 

 

 

 

 

이상으로 redis 테스트 환경 구축하기 마지막인 test container에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글