문제
문제 상황
@BeforeEach로 테스트 환경을 설정하는 메서드가 비즈니스 로직을 제대로 반영하지 못하는 문제가 발생한다.
예를 들어 회의 생성 로직에서는 단순히 회의 엔티티를 생성하는 것뿐만 아니라, 회의를 생성한 사람도 회의 참가자 리스트에 포함해야 한다.
하지만 이러한 로직이 테스트 환경 설정 메서드에 반영되지 않을 경우 실제 애플리케이션과 다른 상태에서 테스트가 진행되어 에러를 발생시킬 수 있다.
문제 분석
문제 발생 코드 - createTestMeeting
@BeforeEach에서 호출되어 테스트 환경을 생성하는 메서드이다.
public Long createTestMeeting(Member hostMember) {
Meeting meeting = Meeting.builder()
.hostMember(hostMember)
.title("테스트 회의")
.startTime(LocalDateTime.now().plusHours(1))
.totalEstimatedDuration(Duration.ofHours(1))
.location("테스트 회의실")
.description("테스트 설명")
.imgNum(1)
.build();
return meetingRepository.save(meeting).getId();
}
@BeforeEach
void setup() {
Member member = createTestMember();
UserDetails userDetails = new CustomUserDetails(member);
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
// 메서드 호출
meetingId = createTestMeeting(member);
memberId = member.getId();
}
회의를 생성하는데 단순히 회의 엔티티만 생성하고, 회의 생성 로직 중 하나인 회의 생성자를 참가자 리스트에 포함시키는 로직이 들어가 있지 않다.
이는 비즈니스 로직에 친숙하지 않은 사람이 코드를 짤 경우 충분히 발생할 수 있는 문제이며, 오류를 발생시킬 수 있다.
해결
해결 방법
1. 회의 생성 api를 호출한다
@BeforeEach 메서드에서 직접 회의 생성 api를 호출하면 실제 애플리케이션 동작을 정확하게 반영할 수 있지만 테스트 실행 속도를 느리게 한다.
2. 서비스 레이어의 메서드를 호출한다 => 적용 방법
비즈니스 로직을 직접 호출하므로 비즈니스 로직을 정확하게 반영할 수 있으며, api 호출보다 빠르다.
해결 코드
public Long createTestMeeting(Member hostMember) {
MeetingCreateRequest request = MeetingCreateRequest.builder()
.title("테스트 회의")
.startTime(LocalDateTime.now().plusHours(1))
.estimatedTotalDuration(LocalTime.of(1, 0, 0))
.location("테스트 회의실")
.description("테스트 설명")
.imageNum(1)
.build();
// 서비스 메서드 호출
Meeting meeting = meetingService.createMeeting(request, hostMember);
return meetingRepository.save(meeting).getId();
}
@BeforeEach
void setup() {
Member member = createTestMember();
UserDetails userDetails = new CustomUserDetails(member);
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
// 메서드 호출
meetingId = createTestMeeting(member);
memberId = member.getId();
}
개선 방향 : 공통 테스트 클래스로 분리
위 메서드는 다른 도메인을 테스트할 경우에도 재사용될 것이므로, 공통 클래스로 분리하는 것이 좋다.
이를 통해 코드의 중복을 줄이고 유지 보수 용이성 및 일관성을 보장하자.
공통 클래스로 분리한 코드
public class TestUtil {
private final MemberRepository memberRepository;
private final MeetingService meetingService;
private final MeetingRepository meetingRepository;
public TestUtil(MemberRepository memberRepository, MeetingService meetingService,
MeetingRepository meetingRepository) {
this.memberRepository = memberRepository;
this.meetingService = meetingService;
this.meetingRepository = meetingRepository;
}
public void setMemberAuthentication(Member member) {
UserDetails userDetails = new CustomUserDetails(member);
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
public Long createTestMeeting(Member hostMember) {
MeetingCreateRequest request = MeetingCreateRequest.builder()
.title("테스트 회의")
.startTime(LocalDateTime.now().plusHours(1))
.estimatedTotalDuration(LocalTime.of(1, 0, 0))
.location("테스트 회의실")
.description("테스트 설명")
.imageNum(1)
.build();
Meeting meeting = meetingService.createMeeting(request, hostMember);
return meetingRepository.save(meeting).getId();
}
public Member createTestMember() {
return memberRepository.save(Member.builder()
.role(MemberRole.ROLE_USER)
.name("Test User")
.imageUrl("http://example.com/image.jpg")
.oauthId("oauth123")
.provider(OAuth2Provider.KAKAO)
.fcmToken("fcmToken123")
.imageNum(5)
.build());
}
}
@BeforeEach에서 사용하는 코드를 기능별로 나누어 메서드를 만들고 공통 클래스로 옮겼다.
@Autowired vs 생성자 주입
해당 클래스(TestUtil)는 스프링 컨테이너가 관리하는 Bean이 아니기 때문에 @Autowired를 사용할 수 없다.
대신, 생성자 주입을 사용함으로써 의존성을 명확하게 하고 객체의 불변성을 보장하며 테스트 시 mock 객체를 전달할 수 있다.
테스트 코드
@BeforeEach
void setup() {
// TestUtil 사용
TestUtil testUtil = new TestUtil(memberRepository, meetingService, meetingRepository);
Member member = testUtil.createTestMember();
testUtil.setMemberAuthentication(member);
meetingId = testUtil.createTestMeeting(member);
memberId = member.getId();
}
testUtil의 메서드를 호출하여 사용한다.
가독성도 좋아졌다.
결론
테스트 환경에서 서비스 레이어의 메서드를 호출하는 것은 비즈니스 로직에 부합하는 정확한 테스트 환경을 구성할 수 있게 한다.
또한 공통 테스트 유틸리티 클래스로 분리하여 테스트 코드 재사용성을 높이고 유지보수가 용이하게 하며 일관성을 유지할 수 있다.
Test 1탄 보러가기
https://shout-to-my-mae.tistory.com/406
'문제&해결' 카테고리의 다른 글
[Test] @Mock, @InjectMocks 동작 원리 및 주의 사항 (0) | 2024.04.10 |
---|---|
브라우저 보안 메커니즘 : CORS 정책의 한계 (0) | 2024.03.27 |
Builder 패턴 객체 생성시 필드 초기값 무시되는 문제 (0) | 2024.03.17 |
[Test] 데이터 의존성을 줄인 통합테스트 코드 작성 (MockUser, Spring Security) (0) | 2024.03.17 |
@Value가 단위 테스트에서 동작하지 않는 문제 (생성자 주입, @WebMvcTest) (0) | 2024.03.11 |