개발자는 기록이 답이다
트러블 슈팅 - 테스트코드도 코드이므로 합성을 통해 중복을 없애자 본문
테스트 코드도 코드이기 때문에 중복되는 코드를 리팩토링을 하려고 합니다.
리팩토링하면서 발견한 점과 리마인드해야할 사항에 대해서 정리하고자 합니다
- 테스트 클래스에 @어노테이션은 1개만 존재할 수 있습니다.
- 상속을 단순히 재사용하기 위해 사용하면 많은 문제가 발생합니다.
현재 프로젝트에서 SecurityTest랑 MemberRepositoryTest에서 각 단일 테스트 전에 @BeforeEach어노테이션을 사용하여 Member를 데이터베이스에 저장하는 코드가 중복되어 사용되고 있습니다. 이 코드는 유저 중복을 확인하거나 로그인이나 로그인 실패 테스트를 위해서 사용하기 위해 작성되었습니다.
다른 테스트코드에서도 중복되는 부분을 별도의 상위 클래스를 상속 받도록 구현했기 때문에, 이번 상황도 SetupMemberTestConfig라는 상위클래스를 만들면 될 것이라고 생각했습니다. 그래서 SetupMemberTestConfig를 SecurityTest랑 MemberRepositoryTest에 상속받도록 구현해줬습니다.
하지만 이러한 구조로 진행했을때 SecurityTest는 성공했지만 MemberRepositoryTest에서 Configuration Error가 발생했습니다.
java.lang.IllegalStateException: Configuration error: found multiple declarations of @BootstrapWith for test class [com.flab.offcoupon.repository.MemberRepositoryTest]: [@org.springframework.test.context.BootstrapWith(value=org.mybatis.spring.boot.test.autoconfigure.MybatisTestContextBootstrapper.class), @org.springframework.test.context.BootstrapWith(value=org.springframework.boot.test.context.SpringBootTestContextBootstrapper.class)]
at org.springframework.test.context.BootstrapUtils.resolveExplicitTestContextBootstrapper(BootstrapUtils.java:194)
at org.springframework.test.context.BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.java:150)
at org.springframework.test.context.BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.java:126)
at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:126)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore.lambda$getOrComputeIfAbsent$5(NamespacedHierarchicalStore.java:147)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore$MemoizingSupplier.computeValue(NamespacedHierarchicalStore.java:372)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore$MemoizingSupplier.get(NamespacedHierarchicalStore.java:361)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore$StoredValue.evaluate(NamespacedHierarchicalStore.java:308)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore$StoredValue.access$200(NamespacedHierarchicalStore.java:287)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore.getOrComputeIfAbsent(NamespacedHierarchicalStore.java:149)
at org.junit.platform.engine.support.store.NamespacedHierarchicalStore.getOrComputeIfAbsent(NamespacedHierarchicalStore.java:168)
해당 오류는 테스트 클래스에 2개 이상의 @BootstrapWith 어노테이션이 있는 경우에 발생합니다.
공식문서를 살펴보면 일반적으로 @BootstrapWith 어노테이션은 테스트 컨텍스트 프레임워크(Spring TestContext Framework)가 테스트 클래스의 컨텍스트를 설정할때 사용됩니다. 그래서 한 클래스에 1번만 사용되어야 합니다.
이 어노테이션은 특정한 TestContextBootstrapper를 지정해 해당 테스트 클래스의 컨텍스트를 설정하는데 사용됩니다.
각 TestContextBootstrapper는 특정한 컨텍스트 구성 방법을 지원하며, 클래스에 여러 @BootstrapWith 어노테이션이 있을 경우 어떤 스프링이 TestContextBootstrapper을 사용할지 결정할 수 없기 때문에 에러가 발생합니다.
MemberRepositoryTest 클래스에서 @BootstrapWith 어노테이션을 두 번 사용하고 있습니다.
- org.mybatis.spring.boot.test.autoconfigure.MybatisTestContextBootstrapper
- org.springframework.boot.test.context.SpringBootTestContextBootstrapper
어디서 사용되고 있는지 다시 한번 어노테이션을 뜯어보겠습니다.
현재 클래스에서 사용되고 있는 @MybatisTest와 @SpringBootTest 어노테이션을 살펴보면 둘다 @BootstrapWith 어노테이션을 사용하고 있는걸 확인할 수 있습니다.
SecurityTest에서는 실제 애플리케이션 컨텍스트가 필요하기 때문에 @SpringBootTest이 필요하지만, MemberRepositoryTest에서는 단순히 Repository를 Mocking하여 테스트하기 때문에 @SpringBootTest가 필요없는데, 이럴땐 어떻게 구현하면 좋을까요?
객체지향적으로 생각해봅시다. 일단 SetupMemberTestConfig파일은 is kind of a 관계가 아니라 has a 관계가 더 맞다고 볼 수 있겠죠.
설정 클래스로서 SecurityTest와 MemberRepositoryTest를 테스트 하기 위해 사전 작업이기 때문입니다.
그러면 단순히 재사용을 목적으로 상속을 사용하면 안됩니다. 공통적인 부분을 추상화하여 합성으로 사용할 수 있도록 변경해보겠습니다.
먼저 SetupMemberTestConfig 클래스는 Member관련된 초기화 뿐만아니라 다른 쿠폰, 이벤트 초기화도 포함할 예정이기 때문에 SetupUtils로 변경하겠습니다. 그리고 생성자를통해서 DTO를 만들도록 해주고, setUpMember메소드에서 인자로 Service Layer를 받을지, Repository Layer를 받을지 선택적으로 적용해서 security를 이용한 회원가입을 진행할 것인지, 단순히 데이터베이스에 저장할 것인지 정할 수 있도록했습니다.
/**
* 테스트를 위한 추상화된 공통 유틸리티 클래스입니다.
* 회원 가입, 이벤트 및 쿠폰 설정 등의 테스트 시 공통적으로 사용되는 기능을 제공합니다.
*/
public class SetupUtils {
private SignupMemberRequestDto testSignupMemberDto;
...
/**
* 기본 생성자 - 테스트용 회원 가입 DTO를 초기화합니다.
*/
public SetupUtils() {
this.testSignupMemberDto = SignupMemberRequestDto.create(
"test@gmail.com",
"ababab123123",
"이름",
LocalDate.now(),
"01012345678",
Role.ROLE_USER
);
}
/**
* 주어진 MemberService를 사용하여 테스트용 회원을 가입시킵니다.
*
* @param memberService 회원 가입을 처리할 MemberService 객체
*/
public void setUpMember(MemberService memberService) {
memberService.signUp(testSignupMemberDto);
}
/**
* 주어진 MemberRepository를 사용하여 테스트용 회원을 저장합니다.
*
* @param memberRepository 회원을 저장할 MemberRepository 객체
*/
public void setUpMember(MemberRepository memberRepository) {
memberRepository.save(Member.create(
testSignupMemberDto.getEmail(),
testSignupMemberDto.getPassword(),
testSignupMemberDto.getName(),
testSignupMemberDto.getBirthdate(),
testSignupMemberDto.getPhone(),
testSignupMemberDto.getRole()));
}
...
}
그리고 SetupUtils클래스를 상속이 아니라 합성으로 사용할 수 있도록 변경해보겠습니다.
이러면 @BootsstrapWith 어노테이션이 중복되지 않고도 코드 중복을 피할 수 있습니다.
결론
- 테스트 코드도 코드이기 때문에 초기화 과정에서 중복을 없애기 위해 리팩토링 해야 합니다.
- 상속을 사용한다면 부모 클래스에 있는 불필요한 내용까지 자식 클래스에 물려져오는 문제가 발생할 수 있습니다.
- 합성을 통해 관련 있는 기능을 별도의 클래스로 분리하여 사용함으로써 코드의 결합도를 낮추고 유연한 구조를 유지할 수 있습니다.
'Spring > 트러블 슈팅' 카테고리의 다른 글
비동기 쿠폰 발급 알림 기능에서 SSE를 선택한 이유 (0) | 2024.03.25 |
---|---|
@UtilityClass란? (0) | 2024.03.25 |
트러블 슈팅 - Junit 테스트를 하다가 Lock wait timeout exceeded 에러 발생. (0) | 2024.03.13 |
트러블 슈팅 - private 메소드를 테스트하려 했지만, 문제는 테스트 코드 로직이었다. (0) | 2024.03.13 |
쿠폰 발급에 대한 동시성 처리 (2) - MySQL의 NamedLock, Redis의 분산락(Lettuce, Redisson) (0) | 2024.03.02 |