본문 바로가기

Project/구현

[Tickerbell] 테스트 시 S3, Redis 목킹하기

TickerBell 프로젝트를 진행하며 공연의 사진을 등록할 때 AWS의 S3를 이용해 사진을 저장해왔다. 나는 이벤트 등록 api를 개발했기에 해당 api에 대한 테스트 코드역시 MockMvc를 이용해 작성했다. 하지만 테스트에 사용된 이미지 파일을 그대로 S3에 올리는 것은 적절하지 않다고 판단했다. 만약 테스트 코드를 실행할 때 이미지 파일을 모두 S3에 올린다면 이는 엄청난 S3 자원에 낭비가 될 것이라고 생각했다. 또한 테스트시에 S3와 같은 외부환경을 의존하는것도 좋지 않은 것 같았다.

 

실제 AWS의 S3를 의존하지 않기 위해 구글 검색을 통해 방법을 찾아보았다. 처음 적용한 방법으로 build.gradle에 S3를 목킹하기 위한 라이브러리 의존성을 추가했다.

// S3 Mock
testImplementation 'io.findify:s3mock_2.12:0.2.4'

이후 test 패키지 아래 다음과 같이 코드를 작성했다.

@Profile("test")
@Configuration
public class MockS3Config {

    @Value("${cloud.aws.region.static}")
    private String region;
    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    @Bean
    public S3Mock s3Mock() {
        S3Mock s3Mock = new S3Mock.Builder().withPort(8001).withInMemoryBackend().build();
        s3Mock.start(); // S3Mock 시작
        return s3Mock;
    }

    @Bean
    @Primary
    public AmazonS3Client amazonS3(S3Mock s3Mock) {
        AwsClientBuilder.EndpointConfiguration endpoint =
                new AwsClientBuilder.EndpointConfiguration("http://localhost:8001", region);

        AmazonS3Client client = (AmazonS3Client) AmazonS3ClientBuilder
                .standard()
                .withPathStyleAccessEnabled(true)
                .withEndpointConfiguration(endpoint)
                .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials()))
                .build();

        client.createBucket(bucket);
        return client;
    }
}

해당 테스트 Config를 작성한 뒤 이미지를 등록하는 API 테스트 코드를 실행하니 정상 동작했으며 실제 S3에는 어떠한 파일도 저장되지 않았다(여담으로 테스트가 아닌 실제 S3Config 에서 빈으로 등록할 때 반환 객체로 AmazonS3Client를 반환했는데 테스트 Config에 AmazonS3Client가 아닌 AmazonS3를 반환했다가 테스트에 계속 실패해 오랜시간 삽질했다..)

실제 S3와 테스트환경을 잘 분리했다고 생각하며 뿌듯한 마음으로 전체 테스트 코드를 실행해보았는데..

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.findify.s3mock.S3Mock]: Factory method 's3Mock' threw exception with message: Bind failed because of Address already in use
Caused by: akka.stream.impl.io.ConnectionSourceStage$$anon$2$$anon$1: Bind failed because of Address already in use
Caused by: java.net.BindException: Address already in use

 

다른 API 테스트들에서 해당 오류들이 터져나왔다. MockS3Config에서 lcoalhost:8001을 이용해 로컬 PC에서 S3처럼 사용할 수 있게 해주었는데 테스트가 끝나면 이를 반환하지 않고 다른 API 테스트가 실행될 때 또 같은 URL로 객체를 생성하려 했기에 오류가 발생했다.

테스트가 끝날 때 마다 해당 설정을 삭제하고 다른 테스트가 실행될 때 다시 설정 클래스가 동작하게 하려했으나 이는 너무 머리가 아팠다.

 

그러다 현재 테스트에서 문제되는건 실제 사진이 저장된 S3의 URL을 가져오는 부분이었기에 이에 해당하는 코드만 목킹하면 됐다. 복잡하게 다른 라이브러리를 사용하지 않고 단순히 Mockito의 Mockito.mock을 이용해 이를 해결했다.

@Profile("test")
@Configuration
public class MockS3Config {

    @Bean
    @Primary
    public AmazonS3Client amazonS3ClientMock() throws MalformedURLException {
        AmazonS3Client mock = Mockito.mock(AmazonS3Client.class);

        when(mock.getUrl(anyString(), anyString())).thenReturn(new URL("https://example.com/yourBucketName/yourFileName"));

        return mock;
    }
}

해당 코드 역시 테스트 패키지에 개발했으며 test 환경에서만 동작하도록 설정했다. 해당 코드를 이용해 S3에서 URL을 가져오는것 처럼 동작하게 할 수 있었으며 여러 테스트들과 함께 실행했을 때도 정상적으로 작동했다.

 

S3를 목킹하다보니 RefreshToken을 저장하기 위해 Redis를 사용했는데 Redis 역시 실제 사용하는 Redis와 분리돼야한다고 생각했다. 곧바로 구글에 방법을 찾아보았으며 해결책은 다음가 같았다.

// redis Mock
testImplementation "org.testcontainers:testcontainers:1.19.1"
testImplementation "org.testcontainers:junit-jupiter:1.19.1"

build.gralde에 testcontainer 의존성을 추가해준다.

@Profile("test")
@Configuration
@Testcontainers
public class MockRedisConfig {

    @Container
    public static GenericContainer<?> redis = new GenericContainer<>("redis:latest").withExposedPorts(6379);

    @Bean
    @Primary
    public RedisConnectionFactory redisConnectionFactoryMock() {
        redis.start();
        return new LettuceConnectionFactory(redis.getHost(), redis.getFirstMappedPort());
    }

    @Bean
    @Primary
    public RedisTemplate<String, String> redisTemplateMock() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        // setKeySerializer, setValueSerializer 설정
        // redis-cli을 통해 직접 데이터를 조회 시 알아볼 수 없는 형태로 출력되는 것을 방지
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactoryMock());

        return redisTemplate;
    }
}

해당 코드역시 테스트 페키지에 개발된 코드이며 @TestContainers, @Container 어노테이션과 함께 테스트시에 사용할 도커 이미지를 선언한다. 아래에는 테스트 환경에서 @Container를 이용해 실행한 Redis 이미지를 사용할 수 있도록 @Primary를 이용해 Bean 사용시 우선순위를 높였다.

 

이후 Redis에 토큰을 저장하는 테스트를 실행하니 기존에 사용하던 Redis 컨테이너에 값이 저장되지 않았으며 테스트 실행 시 기존에 사용하던 컨테이너 이외에 테스트 컨테이너가 도커에 등록됐다가 삭제되는것을 볼 수 있었다.