Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] - BE 테스트 개선 2단계 Testcontainers 도입 #623

Merged
merged 22 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
532ee25
feat: testcontainers 의존성 추가
eunjungL Dec 15, 2024
25278dd
feat: default profile logback CONSOLE appender 추가
eunjungL Dec 15, 2024
de423e7
feat: 내장 H2 자동 활성화 제거
eunjungL Dec 18, 2024
85e60ba
feat: test datasource mysql testcontainers 도입
eunjungL Dec 18, 2024
8a57989
fix: MySQL 문법 오류 수정
eunjungL Dec 18, 2024
fa33ba9
feat: 테스트 컨테이너에 초기화 시 flyway가 실행되도록 설정
Libienz Dec 18, 2024
7e8651b
feat: 테스트컨테이너 localstack 의존성 추가
Libienz Dec 18, 2024
eff6bd7
refactor: 내장 S3 Mocking 설정이 local 환경에만 적용되도록 수정
Libienz Dec 18, 2024
5af024b
feat: LocalStackContainer를 사용하는 S3TestConfig 작성
Libienz Dec 18, 2024
3ad2d73
feat: 테스트 관련 s3 클라우드 속성 값 수정
Libienz Dec 18, 2024
1e83244
test: 서비스 테스트가 테스트 컨테이너로 구성된 S3 설정을 바라보도록 수정
Libienz Dec 18, 2024
658c737
feat: 테스트용 프로파일 이름 test로 설정
Libienz Dec 19, 2024
0d1d484
feat: 기존 테스트에서 사용되는 프로파일 이름 수정 default -> test
Libienz Dec 19, 2024
5d7633a
feat: 각 테스트에서 test 프로파일로 테스트를 실행시키도록 수정
Libienz Dec 19, 2024
6745af0
refactor: s3 업로드 실패 시 stackTrace를 로깅하도록 핸들러 수정
Libienz Dec 19, 2024
283ecb1
fix: 테스트 S3 버킷 images-base-uri 수정
Libienz Dec 19, 2024
02a6a17
fix: 테스트에서 사용되는 Temporary 이미지 경로 영구저장소로 변경
Libienz Dec 19, 2024
cdaf1ce
feat: S3 컨테이너 모킹 로직 제거 및 테스트컨테이너를 사용하도록 수정
Libienz Dec 19, 2024
0aee944
refactor: Testcontainers 설정 IntegrationTest로 통합
eunjungL Dec 19, 2024
96557a0
refactor: S3 Bucket localStackContainer 실행 시 한 번만 생성하도록 변경
eunjungL Dec 19, 2024
3bc490a
feat: Controller, Service 계층 테스트에 IntegrationTest 상속 추가
eunjungL Dec 19, 2024
1074a5a
refactor: IntergrationTest S3 Bucket 이름 상수화
eunjungL Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ dependencies {
testAnnotationProcessor 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// testcontainers
testImplementation "org.testcontainers:testcontainers:1.20.4"
testImplementation "org.testcontainers:junit-jupiter:1.20.4"
testImplementation "org.testcontainers:mysql"
testImplementation 'org.testcontainers:localstack'

// cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;

@Configuration
@Profile({"default", "local"})
@Profile("local")
public class EmbeddedS3Config {

private static final int DYNAMIC_PORT_NUMBER_LOWER = 49152;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public ResponseEntity<ExceptionResponse> handleException(Exception exception) {

@ExceptionHandler(S3UploadException.class)
public ResponseEntity<ExceptionResponse> handleS3UploadException(S3UploadException exception) {
log.warn("S3_UPLOAD_EXCEPTION :: message = {}", exception.getMessage());
log.warn("S3_UPLOAD_EXCEPTION :: stackTrace = ", exception);

ExceptionResponse data = new ExceptionResponse("이미지 업로드에 실패했습니다.");
return ResponseEntity.badRequest()
Expand Down
6 changes: 6 additions & 0 deletions backend/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
</rollingPolicy>
</appender>

<springProfile name="test">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>

<springProfile name="local">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import kr.touroot.authentication.infrastructure.JwtTokenProvider;
import kr.touroot.authentication.infrastructure.KakaoOauthProvider;
import kr.touroot.global.AcceptanceTest;
import kr.touroot.global.IntegrationTest;
import kr.touroot.member.domain.Member;
import kr.touroot.utils.DatabaseCleaner;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -26,7 +27,7 @@

@DisplayName("로그인 컨트롤러")
@AcceptanceTest
class LoginControllerTest {
class LoginControllerTest extends IntegrationTest {

@Autowired
private JwtTokenProvider jwtTokenProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import kr.touroot.authentication.fixture.OauthUserFixture;
import kr.touroot.authentication.infrastructure.JwtTokenProvider;
import kr.touroot.authentication.infrastructure.KakaoOauthProvider;
import kr.touroot.global.IntegrationTest;
import kr.touroot.member.domain.Member;
import kr.touroot.member.fixture.MemberFixture;
import kr.touroot.member.repository.MemberRepository;
Expand All @@ -24,7 +25,7 @@

@DisplayName("로그인 서비스")
@ExtendWith(MockitoExtension.class)
class LoginServiceTest {
class LoginServiceTest extends IntegrationTest {

private static final String AUTHENTICATION_CODE = "test-authentication-code";
private static final String REDIRECT_URI = "http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fv1%2Flogin%2Foauth%2Fkakao";
Expand Down
2 changes: 2 additions & 0 deletions backend/src/test/java/kr/touroot/global/AcceptanceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import java.lang.annotation.RetentionPolicy;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Retention(RetentionPolicy.RUNTIME)
@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"})
@ActiveProfiles("test")
public @interface AcceptanceTest {
}
51 changes: 51 additions & 0 deletions backend/src/test/java/kr/touroot/global/IntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package kr.touroot.global;

import java.io.IOException;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.containers.localstack.LocalStackContainer.Service;
import org.testcontainers.utility.DockerImageName;

public abstract class IntegrationTest {

private static final DockerImageName MYSQL_IMAGE_NAME = DockerImageName.parse("mysql:8");
private static final DockerImageName LOCALSTACK_IMAGE_NAME = DockerImageName.parse("localstack/localstack");
private static final String S3_BUCKET_NAME = "test-bucket";

private static final MySQLContainer<?> mySQLContainer;
private static final LocalStackContainer localStackContainer;

static {
mySQLContainer = new MySQLContainer<>(MYSQL_IMAGE_NAME);
localStackContainer = new LocalStackContainer(LOCALSTACK_IMAGE_NAME).withServices(Service.S3);

mySQLContainer.start();
localStackContainer.start();

createS3Bucket();
}

private static void createS3Bucket() {
try {
localStackContainer.execInContainer("awslocal", "s3", "mb", "s3://" + S3_BUCKET_NAME);
} catch (IOException | InterruptedException e) {
throw new RuntimeException("S3 버킷을 생성하던 중 오류가 발생했습니다.");
}
}

@DynamicPropertySource
private static void dynamicProperties(DynamicPropertyRegistry registry) {
// add MySQL Properties
registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
registry.add("spring.datasource.driver-class-name", mySQLContainer::getDriverClassName);
registry.add("spring.datasource.username", mySQLContainer::getUsername);
registry.add("spring.datasource.password", mySQLContainer::getPassword);

// add S3 Properties
registry.add("cloud.aws.s3.access-key", localStackContainer::getAccessKey);
registry.add("cloud.aws.s3.secret-key", localStackContainer::getSecretKey);
registry.add("cloud.aws.s3.endpoint", () -> localStackContainer.getEndpointOverride(Service.S3));
}
}
4 changes: 4 additions & 0 deletions backend/src/test/java/kr/touroot/global/ServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import kr.touroot.utils.DatabaseCleaner;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Import(value = {DatabaseCleaner.class})
@Retention(RetentionPolicy.RUNTIME)
@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"})
@ActiveProfiles("test")
public @interface ServiceTest {
}
41 changes: 41 additions & 0 deletions backend/src/test/java/kr/touroot/global/config/S3TestConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kr.touroot.global.config;

import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

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

private final String s3AccessKey;
private final String s3SecretKey;
private final String s3Endpoint;

public S3TestConfig(
@Value("${cloud.aws.s3.access-key}") String s3AccessKey,
@Value("${cloud.aws.s3.secret-key}") String s3SecretKey,
@Value("${cloud.aws.s3.endpoint}") String s3Endpoint
) {
this.s3AccessKey = s3AccessKey;
this.s3SecretKey = s3SecretKey;
this.s3Endpoint = s3Endpoint;
}

@Bean(name = "s3Client", destroyMethod = "close")
public S3Client s3Client() {
AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(s3AccessKey, s3SecretKey);

return S3Client.builder()
.region(Region.AP_NORTHEAST_2)
.endpointOverride(URI.create(s3Endpoint))
.credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import java.util.List;
import kr.touroot.global.AcceptanceTest;
import kr.touroot.global.IntegrationTest;
import kr.touroot.global.exception.S3UploadException;
import kr.touroot.image.domain.ImageFile;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -14,8 +15,9 @@
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

@DisplayName("Aws S3 프로바이더")
@AcceptanceTest
class AwsS3ProviderTest {
class AwsS3ProviderTest extends IntegrationTest {

private final AwsS3Provider s3Provider;
private final String temporaryStoragePath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import kr.touroot.global.AcceptanceTest;
import kr.touroot.global.IntegrationTest;
import kr.touroot.member.dto.request.MemberRequest;
import kr.touroot.member.fixture.MemberFixture;
import kr.touroot.utils.DatabaseCleaner;
Expand All @@ -18,7 +19,7 @@

@DisplayName("사용자 컨트롤러")
@AcceptanceTest
class MemberControllerTest {
class MemberControllerTest extends IntegrationTest {

private final DatabaseCleaner databaseCleaner;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.List;
import kr.touroot.authentication.infrastructure.JwtTokenProvider;
import kr.touroot.global.AcceptanceTest;
import kr.touroot.global.IntegrationTest;
import kr.touroot.image.domain.ImageFile;
import kr.touroot.image.infrastructure.AwsS3Provider;
import kr.touroot.member.domain.Member;
Expand All @@ -25,7 +26,7 @@

@DisplayName("마이 페이지 컨트롤러")
@AcceptanceTest
class MyPageControllerTest {
class MyPageControllerTest extends IntegrationTest {

private final DatabaseCleaner databaseCleaner;
private final JwtTokenProvider jwtTokenProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ public enum MemberFixture {
1L,
null,
null,
"https://dev.touroot.kr/temporary/profile.png",
"https://dev.touroot.kr/images/profile.png",
"리비",
LoginType.KAKAO
),
TOUROOT_LOCAL_USER(
null,
"[email protected]",
"password",
"https://dev.touroot.kr/temporary/profile.png",
"https://dev.touroot.kr/images/profile.png",
"뚜리",
LoginType.DEFAULT
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

import java.util.List;
import kr.touroot.authentication.infrastructure.PasswordEncryptor;
import kr.touroot.global.IntegrationTest;
import kr.touroot.global.ServiceTest;
import kr.touroot.global.auth.dto.MemberAuth;
import kr.touroot.global.config.EmbeddedS3Config;
import kr.touroot.global.config.S3TestConfig;
import kr.touroot.global.exception.BadRequestException;
import kr.touroot.image.domain.ImageFile;
import kr.touroot.image.infrastructure.AwsS3Provider;
Expand All @@ -26,11 +27,17 @@
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;


@DisplayName("사용자 서비스")
@Import(value = {MemberService.class, MemberTestHelper.class, PasswordEncryptor.class, AwsS3Provider.class,
EmbeddedS3Config.class})
@Import(value = {
MemberService.class,
MemberTestHelper.class,
PasswordEncryptor.class,
AwsS3Provider.class,
S3TestConfig.class
})
@ServiceTest
class MemberServiceTest {
class MemberServiceTest extends IntegrationTest {

private final MemberService memberService;
private final MemberTestHelper testHelper;
Expand Down
Loading
Loading