스프링 이벤트 리스너 테스트를 위한 @SpyBean과 @MockBean의 활용
📌 서론
이전 글에서는 @SpyBean의 기본적인 개념과 역할에 대해 알아보았다. 이번 글에서는 스프링 이벤트 리스너 클래스를 테스트하는 데 @SpyBean과 @MockBean을 어떻게 활용할 수 있는지 구체적으로 살펴보자. 특히, 이벤트 리스너의 호출 여부와 반응을 확인하는 방법에 초점을 맞추어볼 것이다.
🔻 @MockBean과 @SpyBean에 대해서 잘 모른다면 아래 글을 참고하면 좋다. 🔻
@MockBean과 @Mock의 차이 (+ @InjectMocks)
스프링 테스트 코드에서 실제 호출 @SpyBean으로 확인하기
1. 스프링 이벤트 리스너
스프링 이벤트 리스너 테스트 코드 작성 전, 실제 리스너 클래스 구조를 살펴보자.
@Slf4j
@RequiredArgsConstructor
@Component
public class SpringEventVerifyCodeListener {
private final RedisService redisService;
private final Duration TIMEOUT = Duration.ofMinutes(5); // 5분
@EventListener
public void eventVerifyCodeListener(SendVerifyCodeSpringEvent event) {
String phoneNumber = event.phoneNumber();
String verificationCode = event.verificationCode();
// Redis에 저장 (키: 전화번호, 값: 인증코드, 만료 시간: 5분)
redisService.setValues(phoneNumber, verificationCode, TIMEOUT);
log.info("Saved in Redis with expiration: key {}, value {}, duration {}", phoneNumber, verificationCode, TIMEOUT);
}
}
SpringEventVerifyCodeListener 클래스는 SendVerifyCodeSpringEvent 이벤트를 수신하고 처리하는 로직을 포함한다. 이 이벤트는 '사용자 번호로 인증 코드를 전송'한 후 발행되며, 리스너는 전달된 전화번호(phoneNumber)와 인증코드(verificationCode)를 Redis에 저장하는 역할을 수행한다.
2. @SpyBean을 이용한 이벤트 리스너 클래스 테스트 - 통합 테스트
@SpyBean을 활용해 이벤트 리스너의 호출과 반응을 테스트하는 것은 스프링 기반 애플리케이션에서 중요하다. 이벤트 기반 프로그래밍에서 특정 이벤트 발생 시 리스너의 올바른 반응을 검증하는 것은 필수적이다.
이벤트 리스너 테스트의 목적
이벤트 리스너 테스트의 주된 목적은 다음과 같다:
- 이벤트 발행 확인: 특정 이벤트가 발행되었을 때, 이벤트 리스너가 이를 감지하고 반응하는지 확인한다.
- 이벤트 처리 로직 검증: 이벤트 리스너가 올바르게 이벤트를 처리하는지, 즉 로직이 예상대로 실행되는지 검증한다.
- 부수 효과 검증: 이벤트 처리가 다른 컴포넌트나 시스템에 예상대로 영향을 미치는지 확인한다.
테스트 시나리오
SpringEventVerifyCodeListener 클래스(스프링 이벤트 리스너 클래스)를 테스트하는 과정은 다음과 같다:
- 리스너와 의존성 모킹: @SpyBean을 사용하여 SpringEventVerifyCodeListener를 스프링 컨텍스트에 등록하고, @MockBean을 사용하여 리스너가 의존하는 서비스(예: RedisService)를 모킹한다.
- 이벤트 발행: ApplicationContext를 통해 테스트 이벤트를 발행한다.
- 리스너 반응 검증: Mockito의 verify 메서드를 사용하여 리스너의 특정 메서드(예: eventVerifyCodeListener)가 호출되었는지 검증한다.
테스트 코드 예제
@SpringBootTest
class SpringEventVerifyCodeListenerTest extends TotalTestSupport {
@Autowired
private ApplicationContext applicationContext;
@SpyBean
private SpringEventVerifyCodeListener springEventVerifyCodeListener;
@Test
@DisplayName("[happy] 문자 메시지 전송 이벤트 발행시 리스너가 반응하여 동작하는지를 검증한다.")
void whenEventPublished_thenEventListenerIsTriggered() {
// given
SendVerifyCodeSpringEvent event = createSendVerifyCodeSpringEvent();
// when
applicationContext.publishEvent(event);
// then
Mockito.verify(springEventVerifyCodeListener).eventVerifyCodeListener(event);
}
}
이 테스트는 applicationContext.publishEvent(event)를 호출하여 이벤트를 발행하고, Mockito.verify(springEventVerifyCodeListener).eventVerifyCodeListener(event)를 통해 해당 이벤트 리스너가 올바르게 반응했는지 검증한다.
테스트 중요 포인트
이벤트 객체 생성
createSendVerifyCodeSpringEvent() 메서드를 통해 테스트에 사용될 이벤트 객체를 생성한다. 이 객체는 실제 이벤트 리스너가 반응해야 하는 데이터를 포함한다.
이벤트 발행
applicationContext.publishEvent(event)를 사용하여 생성된 이벤트 객체를 스프링 이벤트 버스에 발행한다. 이는 실제 애플리케이션에서 이벤트가 발행되는 방식을 모방한다.
리스너 호출 검증
Mockito.verify(springEventVerifyCodeListener).eventVerifyCodeListener(event)를 사용하여 리스너의 eventVerifyCodeListener 메서드가 실제로 호출되었는지 검증한다. 이는 리스너가 이벤트에 반응하여 예상된 동작을 수행했는지 확인하는 중요한 단계다.
부수 효과 검증
필요한 경우, 리스너의 처리 결과로 발생할 수 있는 부수 효과(예: 데이터베이스 상태 변경, 외부 시스템 호출 등)도 검증한다. 이는 @MockBean으로 모킹된 의존성(예: RedisService)을 사용하여 수행할 수 있다.
@SpyBean의 필수성 이유
이벤트 리스너 클래스를 테스트할 때 @SpyBean은 필수적인 부분이다. 그 이유는 다음과 같다:
- 실제 동작 유지: @SpyBean은 실제 빈의 인스턴스를 사용하므로, 애플리케이션의 실제 동작을 그대로 유지한다. 이는 테스트 과정에서 리스너의 실제 로직과 행동을 정확하게 평가할 수 있게 한다.
- 부분적 오버라이딩: @SpyBean을 사용하면, 필요한 부분만 오버라이딩하여 테스트할 수 있다. 예를 들어, 리스너의 특정 메서드만 모킹 하거나 특정 상황에서의 동작을 변경할 수 있다. 이는 테스트의 유연성을 크게 향상시킨다.
- 통합 테스트와의 균형: @SpyBean은 통합 테스트 환경에서 특히 유용하다. 리스너가 다른 컴포넌트와 상호작용하는 전체적인 프로세스를 테스트하면서도, 특정 부분의 동작을 조절할 수 있다.
- 실제 컴포넌트와의 상호작용: 리스너가 다른 실제 빈과 상호작용하는 경우, @SpyBean은 이 상호작용을 실제 환경과 유사하게 재현한다. 이는 테스트가 실제 운영 환경에서의 리스너 동작을 더 잘 반영하게 해 준다.
이러한 이유로, @SpyBean은 스프링 이벤트 리스너 테스트에서 중요한 역할을 수행한다. 리스너의 실제 동작을 검증하고, 테스트의 유연성을 제공하며, 실제 운영 환경에 가까운 테스트를 가능하게 하는 것이다.
이제 @MockBean에 대해서도 알아보자.
3. @MockBean을 이용한 의존성 대체와 테스트 - 통합 테스트
@MockBean은 테스트 중에 스프링 컨텍스트에 등록된 실제 빈을 모의 객체로 대체한다. 이는 리스너가 의존하는 서비스나 컴포넌트의 실제 구현을 테스트에서 격리시키는 데 유용하다.
@MockBean의 역할
@MockBean은 다음과 같은 역할을 한다:
- 의존성 격리: 테스트 대상인 리스너와 다른 빈 간의 상호작용을 격리시킨다. 이를 통해 테스트가 다른 빈의 구현 세부사항에 의존하지 않도록 보장한다.
- 환경 통제: 테스트 중에 리스너가 상호작용하는 외부 서비스나 컴포넌트의 동작을 모의 객체를 통해 통제한다. 이를 통해 예측 가능한 테스트 환경을 구성한다.
- 부수 효과 분리: 리스너의 동작이 다른 컴포넌트에 미치는 부수 효과를 모의 객체로 분리함으로써 테스트의 정확성을 높인다.
@MockBean을 사용한 테스트 시나리오
SpringEventVerifyCodeListener 클래스에서 RedisService와의 상호작용을 @MockBean을 통해 모킹하여 테스트하는 방법은 다음과 같다:
- 모킹 설정: @MockBean을 사용하여 RedisService를 모킹하고, @SpyBean 또는 @Autowired를 사용하여 SpringEventVerifyCodeListener를 스프링 컨텍스트에 등록한다.
- 이벤트 발행: ApplicationContext를 통해 이벤트를 발행한다.
- 모킹된 서비스와의 상호작용 검증: Mockito의 verify 또는 when-thenReturn 메서드를 사용하여 RedisService의 특정 메서드(예: setValues)가 적절하게 호출되었는지 검증한다.
이 경우, 실제 Redis 서버와의 통신 없이 메서드 호출과 반환값을 모의할 수 있다.
@SpyBean
private SpringEventVerifyCodeListener springEventVerifyCodeListener;
@MockBean
private RedisService redisService;
@Test
@DisplayName("[happy] 문자 메시지 전송 이벤트 발행 시 Redis 서비스와의 상호작용을 모킹하여 검증한다.")
void whenEventPublished_thenInteractWithMockedRedisService() {
// given
SendVerifyCodeSpringEvent event = createSendVerifyCodeSpringEvent();
when(redisService.setValues(anyString(), anyString(), any(Duration.class))).thenReturn(true);
// when
applicationContext.publishEvent(event);
// then
verify(redisService).setValues(anyString(), anyString(), any(Duration.class));
verify(springEventVerifyCodeListener).eventVerifyCodeListener(event);
}
이 예제에서는 @MockBean을 사용하여 RedisService의 setValues 메서드 호출을 모킹하고 있다. 이렇게 함으로써, 실제 Redis 서버에 접근하지 않고도 이벤트 리스너가 RedisService와 올바르게 상호작용하는지 테스트할 수 있다.
4. 결론
결론적으로, @SpyBean과 @MockBean을 조합하여 사용함으로써, 스프링 기반의 복잡한 컴포넌트인 이벤트 리스너의 테스트를 효과적으로 수행할 수 있다. @SpyBean은 리스너의 실제 동작과 상호작용을 유지하면서 테스트의 유연성을 제공하고, @MockBean은 리스너가 의존하는 외부 서비스나 컴포넌트를 모킹하여 테스트 환경의 격리와 통제를 가능하게 한다. 이러한 방식은 테스트의 정확성과 신뢰성을 높이며, 실제 운영 환경에서의 리스너 동작을 효과적으로 모의할 수 있도록 한다.
테스트 전략의 이러한 조합은 스프링 애플리케이션의 복잡한 구조와 상호작용을 가진 컴포넌트를 테스트하는 데 있어 매우 중요하다. 이를 통해 개발자는 애플리케이션의 각 부분이 어떻게 함께 작동하는지, 그리고 각 컴포넌트가 예상대로 기능하는지를 보다 정확하게 파악할 수 있다.
@SpyBean과 @MockBean의 사용은 테스트의 복잡성을 관리하고, 다양한 시나리오와 환경에서 애플리케이션의 동작을 검증하는 데 중요한 도구이다. 이를 통해 개발자는 스프링 애플리케이션의 신뢰성과 안정성을 효과적으로 보장할 수 있으며, 지속 가능한 개발과 유지보수를 위한 견고한 기반을 마련할 수 있다.
Mockito when().thenReturn()에서 파라미터로 eq()와 직접 변수 사용의 차이점 이해하기
📣 이 글은 내가 소속된 Team Chillwave에서 진행한 사이드 프로젝트에 적용한 내용을 다시 공부하고 정리한 것이다.
다른 팀원인 "개발자의 서랍" 님의 블로그도 방문하면 도움이 될 것 같다 :)
'Spring > Spring 테스트코드' 카테고리의 다른 글
Mockito when().thenReturn()에서 파라미터로 eq()와 직접 변수 사용의 차이점 이해하기 (0) | 2024.01.11 |
---|---|
테스트 격리성의 중요성과 모의 객체(Mock Object)를 활용한 전략 (3) | 2024.01.01 |
스프링 테스트 코드에서 실제 호출 @SpyBean으로 확인하기 (3) | 2023.12.31 |
스프링 테스트 코드: @MockBean과 @Mock의 차이 (+ @InjectMocks) (1) | 2023.12.31 |
테스트 코드의 가독성 향상 - BDD 방법론과 @DisplayName 어노테이션 활용 (2) | 2023.12.26 |