728x90
문제
문제 상황
데이터 의존성
테스트 시 데이터베이스의 데이터 상태에 따라 테스트 통과 여부가 달라진다.
즉, 테스트 결과가 데이터베이스의 테스트 데이터에 의존적이다.
Spring Security 보안 컨텍스트
애플리케이션에서 Spring Security를 사용할 경우 테스트시 보안 컨텍스트를 설정하고 jwt 토큰에 넣을 테스트 사용자가 데이터베이스에 존재해야한다.
CustomMockUser
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username() default "1";
String role() default "USER";
}
보안 컨텍스트 설정
@Component
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {
@Autowired
private MemberRepository memberRepository;
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
// @BeforeEach로 멤버 생성해도 찾을 수 없는 에러 발생
Member member = memberRepository.findByOauthId("oauth123") // 테스트 멤버
.orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MemberId", "Member not found")));
CustomUserDetails userDetails = new CustomUserDetails(member);
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
context.setAuthentication(auth);
return context;
}
}
커스텀 어노테이션인 @WithMockCustomerUser로 보안 컨텍스트 설정시 @BeforeEach보다 먼저 실행된다.
보안 컨텍스트 실행시 @BeforeEach에서 생성한 테스트 사용자가 데이터베이스에 아직 존재하지 않으므로 NotFound 에러가 발생한다.
다른 보안 컨텍스트 설정 방법을 찾아보아야한다.
문제 분석
- 테스트 db의 데이터는 언제든 바뀔 수 있다. 그러므로 테스트 코드 작성시 현재 테스트 데이터베이스 상태를 기준으로 작성해서는 안된다.
- 테스트 어노테이션으로 스프링 보안 컨텍스트 설정시 @BeforeEach보다 먼저 실행되어 필요한 테스트 사용자 데이터가 준비되기 전에 접근 시도할 수 있다.
문제 해결
해결 방법
데이터 준비, 정리
@BeforeEach와 @AfterEach를 사용하여 테스트 시작 전에 데이터를 준비하고, 테스트 종료 후에 데이터를 정리한다.
이 과정을 통해 각 테스트의 독립성을 보장하고 데이터 의존성을 줄일 수 있다.
테스트 메서드로 보안 컨텍스트 설정
@BeforeEach 메서드 내에서 보안 컨텍스트를 직접 설정하여 테스트 사용자 정보를 SecurityContextHolder에 담는다.
이 방법을 통해 테스트 사용자 정보가 데이터베이스에 존재하며 보안 컨텍스트에서도 조회 가능하다.
구현 코드
// 테스트 데이터 메서드
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();
}
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
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();
}
// 테스트 데이터 정리
@AfterEach
void cleanup() {
if (meetingId != null) {
meetingRepository.deleteById(meetingId);
}
if (memberId != null) {
memberRepository.deleteById(memberId);
}
SecurityContextHolder.clearContext();
}
테스트 메서드
@Test
@DisplayName("[PATCH] 회의 종료 API 테스트")
void closeMeeting() throws Exception {
// given
// when
ResultActions perform = mvc.perform(
patch("/api/meetings/" + meetingId + "/end")
.contentType(MediaType.APPLICATION_JSON)
);
// then
perform
.andExpect(status().isOk())
.andDo(print());
}
결론
- @BeforeEach와 @AfterEach를 활용한 데이터 준비와 정리를 통해 테스트 간의 격리와 데이터 의존성을 막는다.
- 테스트 메서드 내에서 직접 보안 컨텍스트를 설정하여 테스트 유저가 db에 존재하고, 로직을 테스트 할 수 있다.
Test 2탄
https://shout-to-my-mae.tistory.com/408
728x90
'문제&해결' 카테고리의 다른 글
[Test] 비즈니스 로직 정확도 향상 전략 및 공통 테스트 유틸리티 클래스 활용 (0) | 2024.03.19 |
---|---|
Builder 패턴 객체 생성시 필드 초기값 무시되는 문제 (0) | 2024.03.17 |
@Value가 단위 테스트에서 동작하지 않는 문제 (생성자 주입, @WebMvcTest) (0) | 2024.03.11 |
도커 컨테이너의 ip 주소는 왜 127.0.0.1(localhost)가 아닐까? (0) | 2024.03.09 |
로컬, docker 컨테이너 포트 충돌 : 포트 관리 중요성 (mysql) (0) | 2024.03.09 |