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

[REFACTOR] 테스트코드에서 Testcontainers를 사용하도록 변경 #840

Open
zeus6768 opened this issue Oct 21, 2024 · 3 comments
Open
Assignees
Labels
BE 백엔드 refactor 요구사항이 바뀌지 않은 변경사항

Comments

@zeus6768
Copy link
Contributor

zeus6768 commented Oct 21, 2024

📌 어떤 기능을 리팩터링 하나요?

현재 테스트 환경의 applcation.yml 파일을 gitignore에 등록해야 테스트를 정상적으로 실행할 수 있습니다.

다음의 문제가 있습니다.

  • 변경 내역에 잡히지 않는 파일을 관리해야 합니다.
  • CI 과정에서 yml 파일을 생성해야 합니다. 이 또한 관리 대상입니다.

그래서 테스트 코드를 실행할 때, 로컬 DB를 사용하는 대신 Testcontainers를 사용하도록 변경합니다.

생각해볼점

테스트컨테이너를 사용할 경우와 사용하지 않을 경우의 트레이드오프는 뚜렷해요.

사용할 경우 application.yml, CI 환경변수 등 테스트 환경 관리 비용이 줄어듦
vs
사용하지 않을 경우 테스트(@SpringBootTest, @DataJpaTest) 속도가 더 빠름

지금 우리의 외부 프로세스 의존성은 MySQL 데이터베이스 뿐이죠.

그러나 캐시를 사용하는 등의 외부 의존성이 추가되면 컨테이너를 더 실행해야 할 수 있어요.

해당 의존성이 통합테스트에서도 필요하니 테스트 패키지의 application.yml 파일과 CI 환경의 application.yml을 관리해줘야겠죠??

Testcontainers는 스프링 코드를 통해 이 과정을 자동화해줘요.

그러나 테스트를 1개를 돌리든 100개를 돌리든 컨테이너를 실행하기 위한 시간이 처음에 30초 ~ 1분 정도 필요해요.

이렇게 될 경우 이미 실행된 컨테이너를 사용하는 기존 방식에 비해 슬라이스테스트, 통합테스트에서 오래 걸리겠죠?

⏳ 예상 소요 시간 (예상 해결 날짜)

8시간

🔍 참고할만한 자료(선택)

Testcontainers Official
Testcontainers | Maven Repository

@zeus6768 zeus6768 added refactor 요구사항이 바뀌지 않은 변경사항 BE 백엔드 labels Oct 21, 2024
@zeus6768 zeus6768 self-assigned this Nov 7, 2024
@zeus6768 zeus6768 added this to the 7차 스프린트 💭 milestone Nov 7, 2024
@zeus6768
Copy link
Contributor Author

zeus6768 commented Dec 29, 2024

Testcontainer 작업 중 떠오른 할 일 1

ServiceTest.java@DatabaseIsolation 제거

원래는 @DatabaseIsolation을 사용해서 테스트 간 저장소를 격리했어요.

처음부터 @Transactional을 사용했다면 그럴 필요 없었어요.

하지만 코드잽은 테스트 코드에 @Transactional을 섣불리 도입하는 것을 경계했답니다!

테스트 코드의 @Transactional 사용은 선배 개발자들 사이에서도 합의되지 않은 주제이기 때문이죠.

관련 자료

그러다 지연 로딩 문제를 만나게 돼요.

TemplateServiceTest 추가 #661

지연 로딩 문제

  • [REFACTOR] Service Layer 리팩토링 #650 (comment)
  • 저도 마찬가지로 짱수와 동일한 프록시 객체 접근 문제가 발생합니다.
  • 몰리와 동일하게 @transactional 어노테이션을 통해 트랜잭션을 생성하는 방식으로 처리했는데, 우선은 프로덕션 코드는 수정되면 안 될 것 같아서 테스트 코드에 @transactional을 추가하는 식으로 해결하였습니다.
  • 추후에 해당 어노테이션을 지우고, 프로덕션 코드에 적용하는 식으로 변경해야 할 것 같습니다.

이때 @Transactional이 도입되었답니다.

두 애너테이션을 함께 사용한다면, 일부 로직이 중복 실행되는 것과 같아요.

  1. @Transactional의 테스트메서드 실행 후 트랜잭션 롤백
  2. @DatabaseIsolation의 테스트메서드 실행 후 DB 초기화

@Transactional이 있으면 굳이 @DatabaseIsolation이 필요 없겠죠?

"테스트메서드 실행 후 DB를 롤백해 원상태로 되돌린다"는 목적을 @Transactional만으로 달성할 수 있으니까요.

두 작업 모두 DB와 통신하기 때문에, 테스트 성능에 큰 방해가 될 거에요. 하나만으로 충분하다고 생각해요.

그런데 @Transactional@DatabaseIsolation과 달리 id counter를 초기화하지 않죠.

테스트는 @DatabaseIsolation의 해당 동작에 의존해서 작성되었기 때문에, ServiceTest.java에서 @DatabaseIsolation를 삭제하면 테스트가 실패해요.

Screenshot 2024-12-29 at 5 07 32 PM

아니면 @Transactional을 지우고 @DatabaseIsolation를 남길 수도 있겠지요.

그런데 이때는 지연로딩 문제를 해결할 다른 방법을 찾아야해요.



AS-IS

@Test
@DisplayName("ID로 템플릿 조회 성공")
void findByTemplateId() {
    // given
    var member = memberRepository.save(MemberFixture.getFirstMember());
    var category = categoryRepository.save(Category.createDefaultCategory(member));
    var template = templateRepository.save(TemplateFixture.get(member, category));

    // when
    var actual = sut.findById(template.getId());

    // then
    assertThat(actual.id()).isEqualTo(1L);
}

현재는 actual의 id를 하드코딩된 값(1L)을 sut의 실행 결과와 비교해 검증하죠.

실제로 실행된 메서드(***repository.save())의 결과값과 비교해야 하지 않을까요?

테스트 메서드 내에서 테스트의 라이프사이클을 최대한 확인할 수 있어야 한다고 생각해요.

그래야 외부 의존성이 변경되더라도 영향을 받지 않으니까요.

그래서 다음과 같이 테스트코드 변경을 제안해요.

TO-BE

@DisplayName("ID로 템플릿 조회 성공")
void findByTemplateId() {
    // given
    var member = memberRepository.save(MemberFixture.getFirstMember());
    var category = categoryRepository.save(Category.createDefaultCategory(member));
    var template = templateRepository.save(TemplateFixture.get(member, category));

    // when
    var actual = sut.findById(template.getId());

    // then
    assertThat(actual.id()).isEqualTo(template.getId());
}

하드코딩된 값(1L)을 검증하는 게 아니라, 실제 로직(***Repository.save())의 결과를 검증해요.

@zeus6768
Copy link
Contributor Author

zeus6768 commented Dec 29, 2024

Testcontainer 작업 중 떠오른 할 일 2

RepositoryTest@DatabaseIsolation 제거

위의 "ServiceTest.java@DatabaseIsolation 제거"와 같은 목적이에요.

@DataJpaTest 애너테이션은 @Transactional 애너테이션이 이미 붙어 있어요.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional   // 여기!!
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
    // 생략
}

여기서도 중복 로직을 수행한다면 테스트 속도가 또 느려질 거에요!

@zeus6768
Copy link
Contributor Author

zeus6768 commented Dec 29, 2024

Testcontainer 작업 중 떠오른 할 일 3

@RepositoryTest를 추상클래스로 변경

Testcontainer를 사용하려면 다음과 같이 @DynamicPropertySource를 사용해서 프로퍼티를 스프링 컨텍스트에 주입해야 해요.

@DynamicPropertySource는 테스트 중에 동적으로 **환경 프로퍼티(properties)**를 설정할 수 있도록 지원해요.

application.yml에 정의하던 값들을, test container의 값들로 대체하는 거에요.

이 작업은 DB를 임시로 생성된 MySQL 컨테이너를 data source로써 연결할 수 있게 해줘요.

@DynamicPropertySource
public static void setProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.write.jdbc-url", MYSQL_CONTAINER::getJdbcUrl);
    registry.add("spring.datasource.write.username", MYSQL_CONTAINER::getUsername);
    registry.add("spring.datasource.write.password", MYSQL_CONTAINER::getPassword);
    registry.add("spring.datasource.read.jdbc-url", MYSQL_CONTAINER::getJdbcUrl);
    registry.add("spring.datasource.read.username", MYSQL_CONTAINER::getUsername);
    registry.add("spring.datasource.read.password", MYSQL_CONTAINER::getPassword);
}

그런데 @DynamicPropertySource 애너테이션은 스프링 컨텍스트가 생성될 때 딱 한 번 실행해야 해요.

즉, @SpringBootTest 또는 @DataJpaTest처럼 @ExtendWith(SpringExtension.class)가 선언된 클래스와 함께 사용되어야 해요.

그러려면 ServiceTest.java에는 다음과 같이 코드가 추가되어야 하겠죠?

@SpringBootTest
@Transactional
public class ServiceTest {

    @Autowired
    protected MemberRepository memberRepository;

    // 생략

    @Autowired
    protected LikesRepository likesRepository;
    
    @DynamicPropertySource
    static void configureDynamicProperties(DynamicPropertyRegistry registry) {
        var container = MySQLTestContainer.getInstance();
        registry.add("spring.datasource.write.jdbc-url", container::getJdbcUrl);
        registry.add("spring.datasource.write.username", container::getUsername);
        registry.add("spring.datasource.write.password", container::getPassword);
        registry.add("spring.datasource.read.jdbc-url", container::getJdbcUrl);
        registry.add("spring.datasource.read.username", container::getUsername);
        registry.add("spring.datasource.read.password", container::getPassword);
    }
}

마찬가지로 RepositoryTest 들에도 해당 설정을 해줘야 해요.

그런데 RepositoryTest 들은 애너테이션 @RepositoryTest를 사용하고 있네요?

각각의 테스트 클래스에 @DynamicPropertySource 설정을 해줘야 하는 상황이에요.

추상 클래스 RepositoryTest를 만들면 좋겠어요!

게다가 ServiceTest는 상속해서 사용하는데, RepositoryTest는 애너테이션으로 사용하는 게 다소 일관성이 떨어지는 것 같으니 둘다 추상클래스로 만들어 사용하면 어떨가요?

AS-IS

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class), useDefaultFilters = false)
@DatabaseIsolation
@Import({JpaAuditingConfiguration.class,
        DataSourceConfig.class,
        QueryDSLConfig.class,
        TemplateSearchExpressionProvider.class,
        FullTextSearchSearchStrategy.class,
        FixedPageCounter.class
})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public @interface RepositoryTest {
}

TO-BE

@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class), useDefaultFilters = false)
@DatabaseIsolation
@Import({JpaAuditingConfiguration.class,
        DataSourceConfig.class,
        QueryDSLConfig.class,
        TemplateSearchExpressionProvider.class,
        FullTextSearchSearchStrategy.class,
        FixedPageCounter.class
})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public abstract class RepositoryTest {

    @DynamicPropertySource
    static void configureDynamicProperties(DynamicPropertyRegistry registry) {
        var container = MySQLTestContainer.getInstance();
        registry.add("spring.datasource.write.jdbc-url", container::getJdbcUrl);
        registry.add("spring.datasource.write.username", container::getUsername);
        registry.add("spring.datasource.write.password", container::getPassword);
        registry.add("spring.datasource.read.jdbc-url", container::getJdbcUrl);
        registry.add("spring.datasource.read.username", container::getUsername);
        registry.add("spring.datasource.read.password", container::getPassword);
    }
}

@RepositoryTest의 애너테이션들은 추상클래스 RepositoryTest에서 여전히 유지할 수 있어요~

@zeus6768 zeus6768 changed the title [REFACTOR] 테스트코드에서 TestContainer를 사용하도록 변경 [REFACTOR] 테스트코드에서 Testcontainers를 사용하도록 변경 Jan 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BE 백엔드 refactor 요구사항이 바뀌지 않은 변경사항
Projects
Status: Todo
Development

No branches or pull requests

1 participant