배경
파일 업로드를 위해 S3를 연결하고 테스트하는 과정에서 문제가 발생했다. S3에 테스트를 위해 생성한 파일들이 계속 저장되어 있던 것!
단위테스트는 Mock을 사용하기 때문에 문제가 없었지만 통합테스트에서는 모든 로직이 그대로 실행되서 저장되는 로직이 포함된 만큼 파일이 S3에 저장됐다.
프리티어를 사용하고 있었기 때문에 GET, POST를 요청을 보낼 수 있는 횟수 제한이 있었고 정신없이 테스트를 돌리다보니 GET 500회, POST 900회나 실행되고 있었다.
이 문제를 해결하기 위해 통합 테스트에서도 S3를 Mock으로 만들어 사용해야 했다.
구현
AwsS3Config
기존에 AmazonS3 Bean을 생성하기 위한 코드이다. 해당 Bean을 사용하지 않도록 Test용 Bean을 만들어서 사용하도록 해야 했다.
@Profile("!test") 애너테이션을 사용해서 프로파일이 test가 아닐 때 사용되도록 했다.
@Configuration
@Profile("!test")
public class AwsS3Config {
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
MockAwsS3Config
테스트 코드에서 AwsS3Config를 대체할 MockAwsS3Config. 해당 코드는 test 환경에서만 사용할거기 때문에 test 디렉토리 쪽에 생성했다.
AwsS3Config를 상속받아 amazonS3Client 메서드를 오버라이딩해서 MockAmazonS3 빈을 생성하도록 재정의했다.
@Configuration
public class MockAwsS3Config extends AwsS3Config {
@Bean
@Override
public AmazonS3 amazonS3Client() {
return Mockito.mock(AmazonS3.class);
}
}
IntergationTest
이제 AmazonS3를 사용하는 통합 테스트에 AmazonS3의 시나리오를 추가해줘야 한다.
이제 Mock 객체로 주입될거기 때문에 시나리오를 추가해주지 않으면 단순히 Null을 반환해서 NPE가 발생하게 된다.
getUrl이 최종적으로 S3에 저장된 파일의 경로를 가져오는 메서드인데 내가 지정해둔 문자열을 반환되도록 했다.
@Slf4j
class MemberIntegrationTest extends BaseIntegrationTest {
...
@Autowired
private MemberService memberService;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private AES128Config aes128Config;
@Autowired
private RedisService redisService;
@Autowired
private AmazonS3 amazonS3;
@BeforeEach
void beforeEach() throws MalformedURLException {
given(amazonS3.putObject(any(PutObjectRequest.class))).willReturn(new PutObjectResult());
given(amazonS3.getUrl(any(), any())).willReturn(
new URL(StubData.CustomMultipartFile.getIMAGE_URL()));
...
}
...
}
public static class CustomMultipartFile {
@Getter
static final String IMAGE_URL =
"https://s3.ap-northeast-2.amazonaws.com/travel-with-me-fileupload/image/example.png";
}
에러 발생
테스트를 실행하자 발생한 에러다. 해당 에러는 PutObjectRequest에 올바른 파라미터를 넣어야된다는 에러인데, 애초에 AmazonS3는 Mock객체이기 때문에 이런 에러가 발생하면 안된다. Mock 객체가 아닌 실제 AmazonS3가 실행되고 있다는 것.
java.lang.IllegalArgumentException: The PutObjectRequest parameter must be specified when uploading an object
at com.amazonaws.services.s3.AmazonS3Client.rejectNull(AmazonS3Client.java:3871)
at com.amazonaws.services.s3.AmazonS3Client.putObject(AmazonS3Client.java:1716)
at com.frog.travelwithme.intergration.member.MemberIntegrationTest.beforeEach(MemberIntegrationTest.java:80)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
해결
AwsS3Config는 Test 프로파일에서는 실행되지 않도록 했는데 왜 실제 AmazonS3가 빈에 등록된걸까?
이유는 MockAwsS3Config가 AwsS3Config를 상속받기 때문이다. 상속받고 있는 AwsS3Config도 Bean을 생성했던 것.
AwsS3Config에서 생성된 빈이 아닌 MockAwsS3Config에서 생성된 빈을 사용하게 만들 방법을 찾아야 했다.
@Primary는 동일한 빈이 있을 때 우선순위를 설정해주는 애너테이션이다. 해당 애너테이션을 통해 MockAwsS3Config에서 생성된 Bean이 주입되도록 우선순위를 줄 수 있었다.
@Configuration
public class MockAwsS3Config extends AwsS3Config {
@Bean
@Primary
@Override
public AmazonS3 amazonS3Client() {
return Mockito.mock(AmazonS3.class);
}
}
테스트
테스트가 문제없이 잘 실행된다. MockHttpServletResponse의 body에서 image 필드를 살펴보면 내가 반환하도록 했던 url이 그대로 잘 들어간 것을 알 수 있다.
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"SAMEORIGIN"]
Content type = application/json
Body = {"data":{"id":5,"email":"e_ma-il@gmail.com","nickname":"nickname","nation":"nation","gender":"남자","image":"https://s3.ap-northeast-2.amazonaws.com/travel-with-me-fileupload/image/example.png","address":"address","introduction":"introduction","role":"USER","createdAt":"2023-05-25T18:05:34.124696","lastModifiedAt":"2023-05-25T18:05:34.124696"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
이제 테스트를 돌릴 때마다 S3의 자원을 사용하지 않게 되었다. 편하게 테스트를 돌려보자
배운 점
1. 통합 테스트에서도 필요에 따라 Mock객체를 사용할 수 있다.
2. @Profile에서 !를 사용해 사용하지 않을 프로파일을 지정할 수도 있다.
3. @Primary 애너테이션을 사용해 동일한 빈이 있을 때 우선순위를 설정할 수 있다.
4. S3 프리티어에는 GET, POST 메서드를 날릴 수 있는 횟수 제한이 있다.
'Spring' 카테고리의 다른 글
Spring - 회원 팔로우 기능 구현 (1) | 2023.06.08 |
---|---|
Spring - Spring Boot 초기 데이터 설정 (data.sql) (0) | 2023.05.29 |
Spring - 좋은 단위 테스트를 만드는 방법(JUnit) (2) | 2023.04.22 |
Spring - 이메일 인증 구현해보기 (랜덤 인증번호 보내기) (9) | 2023.04.21 |
Spring - 로컬 환경을 위한 Embedded Redis 적용하기 (+ Can't start redis server. Check logs for details) (5) | 2023.04.15 |