테스트 코드에서 @MockBean과 @SpyBean을 사용해서 이벤트 리스너 검증을 해보자
📌 서론
이전 포스트에서 열심히 스프링 이벤트 리스너에 대한 테스트 코드를 작성하고 검증했다. 나는 이 2가지 리스너 테스트를 한번에 성공시키고 싶어서 코드를 그대로 합쳐서 테스트를 진행했다. 그런데 테스트에서 오류가 발생했고 이것을 고치는데 생각보다 오랜 시간이 걸렸다.
그 이유는 테스트 코드에 대한 이해가 부족했기 때문인데 특히 @MockBean과 @SpyBean을 함께 사용할 때, 이 두 어노테이션이 어떻게 상호 작용하는지에 대한 이해가 부족했다.
지금부터 이 포스트에서는 두 가지 접근 방식을 비교하면서, 스프링 이벤트 리스너의 반응과 내부 동작을 어떻게 동시에 검증할 수 있는지 내가 겪은 내용을 토대로 설명하도록 하겠다.
이번 포스트는 이전 포스트에서 이어지는 내용입니다. 내용을 읽고 오시는 것을 추천합니다.👇🏻👇🏻
1. 테스트 코드 작성 및 테스트 진행
이전 포스트에서 작성했던 테스트 코드들을 합쳐봤다.
@DisplayName("[통합] RecipeCreate 스프링 이벤트 구독자 테스트")
class RecipeCreateEventListenerTest extends TotalTestSupport {
@Autowired
private ApplicationContext applicationContext;
@MockBean
private RecipeCreateEventListener recipeCreateEventListener;
@SpyBean
private CreateRecipeUseCase createRecipeUseCase;
@Test
@DisplayName("이벤트 발행시 리스너의 외부 반응 검증")
void whenEventPublished_thenEventListenerIsTriggered() {
// given
RecipeCreationEvent event = createEvent();
// when
applicationContext.publishEvent(event);
// then
Mockito.verify(recipeCreateEventListener).saveIngredientsIntoMongo(event);
}
@Test
@DisplayName("이벤트 발행시 리스너의 내부 로직 검증")
void whenEventPublished_thenEventListenerInternalLogicIsExecuted() {
// given
RecipeCreationEvent event = createEvent();
// when
applicationContext.publishEvent(event);
// then
Mockito.verify(createRecipeUseCase).saveIngredientsIntoMongo();
}
private RecipeCreationEvent createEvent() {
return new RecipeCreationEvent("재료", "해시태그");
}
}
지금 작성한 테스트 코드는 @MockBean과 @SpyBean을 다음과 같이 사용한다.
- 이 구성에서 RecipeCreateEventListener 클래스는 모킹(@MockBean)되어 있으므로, 실제 내부 로직은 실행되지 않는다. 결과적으로 CreateRecipeUseCase의 saveIngredientsIntoMongo 메서드는 호출되지 않게 된다.
@MockBean
private RecipeCreateEventListener recipeCreateEventListener;
@SpyBean
private CreateRecipeUseCase createRecipeUseCase;
테스트 결과 아래처럼 오류가 발생했다.
2. 테스트가 실패한 이유 파악하기
테스트가 실패한 이유
- 테스트 환경에서 @MockBean을 사용해 RecipeCreateEventListener를 Mock 객체로 선언하고 있다. 이 Mock 객체로의 선언은 RecipeCreateEventListener의 실제 구현을 무시하고, 대신 Mockito가 추적할 수 있는 대체 객체를 제공한다.
- 이렇게 Mock 객체로 선언해 줬기 때문에 applicationContext.publishEvent(event);를 테스트 코드 내에서 호출하더라도 RecipeCreateEventListener 내부에 구현된 saveIngredientsIntoMongo 메서드는 실제로는 실행되지 않는다.
@MockBean
private RecipeCreateEventListener recipeCreateEventListener;
@SpyBean
private CreateRecipeUseCase createRecipeUseCase;
- saveIngredientsIntoMongo 메서드는 이벤트 리스너의 내부 로직이지만 Mock 객체는 실제 로직을 수행하지 않기 때문에 결과적으로 @SpyBean으로 선언된 CreateRecipeUseCase 클래스 내부의 saveIngredientsIntoMongo 메서드 또한 호출되지 않는다.
@EventListener
public void saveIngredientsIntoMongo(RecipeCreationEvent event) {
// 1. 저장하기 전에 재료를 , 단위로 분리한다.
List<String> ingredients = splitIngredients(event.ingredients());
// 2. 저장을 시도한다.
mongoUseCase.saveIngredientsIntoMongo(ingredients);
log.info("데이터 저장 성공");
}
📌 요약
요약하자면, @MockBean을 사용하여 RecipeCreateEventListener를 Mock 객체로 설정하면, 이 리스너의 실제 로직이 무시되고, 그 결과로 @SpyBean으로 선언된 다른 객체의 메서드 호출 또한 발생하지 않게 된다.
이 내용을 이해하기 쉽게 정리해 봤다.
@MockBean 사용의 영향
- @MockBean을 사용하여 RecipeCreateEventListener를 Mock 객체로 만들면, 이 리스너의 실제 구현은 무시되고, Mockito가 추적하는 Mock 객체가 대신 사용된다. 이 경우 @EventListener로 선언된 saveIngredientsIntoMongo 메서드는 실제 동작을 수행하지 않는다.
테스트 코드 내 동작의 결과
- 테스트에서 applicationContext.publishEvent(event);를 호출해도, 실제 RecipeCreateEventListener의 saveIngredientsIntoMongo 메서드는 실행되지 않는다. 이는 Mock 객체가 실제 로직을 수행하지 않기 때문이다.
@SpyBean과의 상호작용
- @MockBean으로 Mock 객체를 생성하면, 이 객체가 실제 로직을 수행하지 않으므로, @SpyBean으로 선언된 다른 빈(CreateRecipeUseCase)에서 기대하는 메서드(saveIngredientsIntoMongo) 또한 호출되지 않는다.
3. 두 개의 테스트가 통과하도록 코드 수정하기
테스트 코드 수정하기
- 아래와 같이 테스트 코드를 수정했다. 새로운 코드에서는 @SpyBean을 사용하여 RecipeCreateEventListener가 실제 로직을 수행하면서 Mockito가 추적할 수 있도록 설정했다. 이 경우, RecipeCreateEventListener 내부에서 CreateRecipeUseCase의 saveIngredientsIntoMongo 메서드를 호출할 때 이 호출은 실제로 발생하게 되고 Mockito에 의해 추적되며, verify를 통해 검증된다.
@DisplayName("[통합] RecipeCreate 스프링 이벤트 리스너 테스트")
class RecipeCreateEventListenerTest extends TotalTestSupport {
@Autowired
private ApplicationContext applicationContext;
@SpyBean
private RecipeCreateEventListener recipeCreateEventListener;
@MockBean
private CreateRecipeUseCase createRecipeUseCase;
@Test
@DisplayName("이벤트 발행시 리스너의 외부 반응 검증")
void whenEventPublished_thenEventListenerIsTriggered() {
// given
RecipeCreationEvent event = createEvent();
// when
applicationContext.publishEvent(event);
// then
Mockito.verify(recipeCreateEventListener).saveIngredientsIntoMongo(event);
}
@Test
@DisplayName("레시피 생성 스프링 이벤트가 발행되면 이벤트 구독자가 동작한다.")
void whenRecipeCreationEventPublished_thenTriggerEventListenerMethods() {
// given
RecipeCreationEvent event = createEvent();
// when
applicationContext.publishEvent(event);
// then
Mockito.verify(createRecipeUseCase).saveIngredientsIntoMongo();
}
private RecipeCreationEvent createEvent() {
return new RecipeCreationEvent("재료", "해시태그");
}
}
다시 테스트를 해본 결과 성공했다.
4. 두 코드의 차이점 알아보기
첫 번째 테스트 코드는 다음과 같이 작성했다.
- RecipeCreateEventListener는 가짜(@MockBean)로 만들어져서, 실제로는 아무 일도 하지 않는다. 이건 이벤트가 발생하면 실제로 내부의 처리 로직은 실행되지 않는다는 의미다.
- CreateRecipeUseCase는 @SpyBean으로 선언되어서 실제로 동작하면서, 그 동작을 추적할 수 있다. 하지만, RecipeCreateEventListener가 가짜(@MockBean)이기 때문에, 이벤트 리스너에서 CreateRecipeUseCase의 메서드를 호출해도 실제로는 아무 일도 일어나지 않는다.
즉, RecipeCreateEventListener 내부에서 CreateRecipeUseCase의 메서드를 호출해도, 그 호출은 실제로 발생하지 않기 때문에 Mockito가 추적할 수 없고, verify를 통한 검증도 실패하게 된다.
@MockBean
private RecipeCreateEventListener recipeCreateEventListener;
@SpyBean
private CreateRecipeUseCase createRecipeUseCase;
반면, 두 번째 경우에서는 다음과 같이 작성했다.
- 두번째 코드에서는 RecipeCreateEventListener가 @SpyBean으로 선언했기에 메서드가 실제로 동작한다. 이벤트가 발생하면 실제로 내부의 처리 로직이 실행된다는 의미다. 이때 CreateRecipeUseCase는 가짜(@MockBean)로 만들어져서 실제 로직은 실행되지 않지만, 이벤트 리스너에서 호출된다는 것은 추적할 수 있다.
즉, 이제는 RecipeCreateEventListener 내부에서 CreateRecipeUseCase의 메서드를 호출할 때, 그 호출은 Mockito에 의해 추적되고 verify를 통해 검증할 수 있게 된다.
@SpyBean
private RecipeCreateEventListener recipeCreateEventListener;
@MockBean
private CreateRecipeUseCase createRecipeUseCase;
5. 결론: 스프링 테스트에서 @MockBean과 @SpyBean의 중요한 차이점 이해하기
- 스프링 부트에서 테스트를 수행할 때 @MockBean과 @SpyBean은 서로 다른 목적으로 사용된다. @MockBean을 사용하면, 해당 객체는 실제 동작을 하지 않는다. 대신, Mockito는 이 객체에 대한 모든 호출을 추적하고, 이를 기록한다. 이런 접근 방식은 객체가 실제로 어떤 일을 하는지는 중요하지 않고, 단지 호출되는지 여부만 확인하고 싶을 때 유용하다.
- 반면에, @SpyBean을 사용하면, 객체는 자신의 실제 기능을 수행하게 된다. 이때도 Mockito는 이 객체의 모든 활동을 세심하게 추적한다. 이 방법은 객체의 실제 동작이 어떻게 이루어지는지, 그리고 이것이 예상대로 행동하는지를 확인하고자 할 때 적합하다.
📌 마무리
@MockBean과 @SpyBean을 함께 사용할 때는, 코드의 의존성 방향을 주의 깊게 고려하며 적절한 클래스에 해당 어노테이션을 적용하는 것이 중요하다.
'Spring 테스트 코드' 카테고리의 다른 글
Spring 통합테스트: Validation 문제 해결과 깊은 고민 (30) | 2024.01.10 |
---|---|
[Spring] 테스트: @ParameterizedTest 사용방법 (29) | 2023.12.24 |
[Spring] 테스트 코드: @SpyBean으로 @EventListener 검증하기 (40) | 2023.12.23 |
[Spring] 테스트 코드: @MockBean으로 @EventListener 검증하기 (4) | 2023.12.23 |
[Spring] 통합 테스트와 단위 테스트 비교하기 (58) | 2023.12.23 |