단위 테스트를 작성하면서 쓰긴 쓰는데 한참 동안 차이점도 잘 모르고 썼던 것이 바로 @Autowired와 @Mockbean이었다.
둘의 차이점을 정리해보았다.
문제 파악하기
다음과 같은 코드에서 MockBean
을 주입하는 것과 Autowired
를 주입하는 것의 차이는 무엇일까?
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestConfig.class)
class GenericServiceFeignSenderTest {
@Autowired
private GenericServiceFeignSender genericServiceFeignSender;
@MockBean
private ExternalApiClientV1 externalApiClientV1;
@MockBean
private UserTypeResolver userTypeResolver;
}
@Configuration
public class TestConfig {
@Bean
public ExternalApiClientV1 externalApiClientV1() {
return mock(ExternalApiClientV1.class); // Mockito를 사용해 Mock 객체 생성
}
@Bean
public UserTypeResolver userTypeResolver(ApplicationContext applicationContext) {
return new UserTypeResolver(applicationContext);
}
@Bean
public ValidateTransactionAspect validateTransactionAspect() {
return new ValidateTransactionAspect();
}
@Bean
public GenericServiceFeignSender genericServiceFeignSender(ExternalApiClientV1 externalApiClientV1, UserTypeResolver userTypeResolver, ValidateTransactionAspect aspect) throws
Exception {
GenericServiceFeignSender target = new GenericServiceFeignSender(externalApiClientV1, userTypeResolver);
AspectJProxyFactory factory = new AspectJProxyFactory(target);
factory.addAspect(aspect);
return
(GenericServiceFeignSender) factory.getTargetSource().getTarget();
}
}
@Autowired vs. @MockBean
이론적으로 @Autowired
와 @MockBean
은 Spring Framework에서 의존성을 주입하는 방식에 차이가 있다. 그런데 이렇게 말하면 여전히 아리송하다. 구체적으로 살펴보자.
@Autowired
@Autowired
어노테이션은 Spring의 의존성 주입을 위한 어노테이션이다.@Autowired
가 붙은 필드, 생성자, 또는 메서드에 대해 Spring은 해당 타입의 빈을 찾아 자동으로 주입해 준다.- 이는 Spring의 ApplicationContext에서 관리되는 실제의 빈 인스턴스이다.
@Autowired
는 주로 실제 객체를 사용해야 할 때 사용된다.
@MockBean
@MockBean
은 Spring Boot Test에서 제공하는 어노테이션으로, 해당 타입의 빈을 모킹버전으로 교체한다.- 이 모킹된 객체는 실제 로직을 수행하지 않고, 원하는 대로 동작을 설정할 수 있다. (예: 특정 메서드가 호출되면 어떤 값을 리턴하도록 설정)
@MockBean
은 주로 테스트 환경에서 특정 빈의 실제 동작을 원치 않고, 미리 정의된 동작을 수행하게 하기 위해 사용된다.
즉 ...
@Autowired
는 실제 서비스나 구성요소를 주입받아 실제 환경과 유사한 조건에서 테스트하고자 할 때 사용된다.
반면, @MockBean
은 외부 시스템의 호출, 복잡한 로직 등을 모의로 처리하여, 테스트에만 초점을 맞출 수 있게 해준다.
@Autowired
private GenericService genericService; // 실제로 동작하는 GenericService 빈이 주입된다.
@MockBean
private GenericDependency genericDependency; // 실제 로직 대신 Mock 객체가 주입된다.
코드 상황에 따른 차이
- 위 코드에서
ServiceCommunicator
는@Autowired
로 주입되므로, 실제 Spring Context에서 생성된ServiceCommunicator
빈을 사용하게 된다. ClientInterfaceV1
과UserTypeResolver
는@MockBean
으로 주입되므로, 이들은 모킹된 객체가 되어 실제 로직을 수행하지 않는다.
한 마디로 실제 객체와 mock 객체의 차이다.
TestConfig
클래스에서 ClientInterfaceV1
빈은 명시적으로 Mock 객체를 리턴하도록 설정되어 있지만, 테스트 클래스에서 @MockBean
을 사용하면 이 설정은 무시되고 새로운 Mock 객체가 생성된다. 따라서 @MockBean
은 테스트 중에 동적으로 빈을 교체하는 용도로 사용될 수 있다.
위 코드의 예를 살펴보면 Feign
요청을 처리해야 하는데 만약 Client
를 실제로 하면 어떻게 될까? 테스트 코드가 돌아갈 때마다 실제 api
호출이 발생한다. 즉, 특정 서버의 테스트를 위해서 다른 서버의 실제 자원이 사용되어야 하는 것이다. 이러한 의존성은 당연히 바람직하지 못하다. 이렇게 실제 다른 서비스와의 의존성을 배제하기 위해 Client
는 mock
으로 대체하는 것이다.
다음과 같이 정리해보자.
질문: 왜 @MockBean
을 사용하나?
@MockBean
은 실제 빈의 복잡한 로직이나 외부 시스템의 의존성 없이, 특정 빈의 동작을 테스트하는 데에 주로 사용된다.- 예를 들어,
ExternalApiClientV1
의 경우 실제 외부 API 호출을 하지 않으면서도 API 호출의 결과를 시뮬레이션하고 싶을 때 유용하다. @MockBean
을 사용하면, 테스트 중에 해당 빈의 행동을 동적으로 조작할 수 있으며, 다양한 시나리오를 테스트하기 용이하다.
질문: @Autowired
와 @MockBean
을 함께 사용하는 이유는 무엇인가?
@Autowired
와@MockBean
을 함께 사용함으로써, 테스트 대상 클래스(GenericServiceFeignSender
)는 실제 환경에서와 같이 작동하게 하면서, 일부 의존성(예:ExternalApiClientV1
,UserTypeResolver
)은 테스트용 모킹 객체로 대체할 수 있다.- 이렇게 하면, 테스트 대상 클래스가 실제 환경에 가깝게 동작하면서도, 외부 의존성에 대한 복잡성을 줄이고, 테스트의 범위를 좁혀 집중적인 테스팅이 가능해지는 장점이 있다.
예를 들면...GenericServiceFeignSender
클래스가 ExternalApiClientV1
과 UserTypeResolver
에 의존하고 있다고 가정해보자 이러한 의존성들이 실제 외부 서비스와 연결되어 복잡한 로직을 포함하고 있다면, 단위 테스트 작성 시 실제 외부 서비스와의 상호작용이나 로직의 복잡성을 모의(Mock) 객체로 대체함으로써 테스트를 단순화하고 집중할 수 있다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestConfig.class)
class GenericServiceFeignSenderTest {
// 실제 서비스를 주입받음
@Autowired
private GenericServiceFeignSender genericServiceFeignSender;
// 모의 객체로 대체될 의존성
@MockBean
private ExternalApiClientV1 externalApiClientV1;
@MockBean
private UserTypeResolver userTypeResolver;
// 테스트 메서드 예시
@Test
void testGenericServiceFeignSender() {
// 모의 객체의 동작 설정
when(externalApiClientV1.callExternalService(anyString())).thenReturn(new ApiResponse("Mock Response"));
when(userTypeResolver.resolve(any(User.class))).thenReturn(UserType.ADMIN);
// 테스트 수행
Response result = genericServiceFeignSender.performService("testInput");
// 검증
assertEquals("ExpectedResult", result.getContent());
verify(externalApiClientV1).callExternalService("testInput");
verify(userTypeResolver).resolve(any(User.class));
}
}
이 예시에서 GenericServiceFeignSender
의 실제 인스턴스는 @Autowired
를 통해 주입되며, ExternalApiClientV1
와 UserTypeResolver
는 @MockBean
을 통해 모의 객체로 대체된다. 이를 통해 ExternalApiClientV1
에서의 외부 API 호출과 UserTypeResolver
의 사용자 타입 결정 로직을 테스트 내에서 제어할 수 있게 된다.
그런데 Spybean
이라는 것이 또 있다 !
두둥...!
다음과 같은 코드에서 그렇다면 spybean
과 mockbean
의 차이는 또 무엇일까 ?!
간단히만 살펴보자.
@SpringBootTest
@ActiveProfiles("test")
@ExtendWith(MockitoExtension.class)
class TransactionSenderTest {
@Autowired
private TransactionSender transactionSender;
@SpyBean
private CalculatorClientV1 calculatorClientV1;
@SpyBean
private UserTypeResolver userTypeResolver;
@Test
@DisplayName("sendTransactionResponse 성공 케이스 -> 메서드가 유효한 ID로 정상 응답을 반환하는지 검증한다.")
void testSendTransactionResponse() {
// given
RequestPayload requestPayload = FixtureCreator.getRequestPayload();
ServiceResponseDTOV1 serviceResponse = FixtureCreator.getServiceResponse();
TemplateResponseDTOV1 templateResponse = FixtureCreator.getTemplateResponse();
ZoneFeatureCodeVoV1 zoneFeatureCode = FixtureCreator.getZoneFeatureCode();
TransactionResponseDTOV1 expectedResponse = new TransactionResponseDTOV1();
TransactionRequestDTOV1 transactionRequest = TransactionRequestDTOV1.builder()
.transactionId("someTransactionId")
// 필요한 필드를 여기에 계속 추가
.build();
when(userTypeResolver.getId(anyInt(), any(), anyString())).thenReturn("validId-123");
when(calculatorClientV1.createStart(eq("validId-123"), eq(transactionRequest))).thenReturn(new ResponseEntity<>(expectedResponse, HttpStatus.OK));
// when
ResponseEntity<TransactionResponseDTOV1> actualResponse = transactionSender.sendTransactionResponse(requestPayload, serviceResponse, templateResponse, zoneFeatureCode);
// then
assertEquals(HttpStatus.OK, actualResponse.getStatusCode());
assertEquals(expectedResponse, actualResponse.getBody());
}
@Test
@DisplayName("sendTransactionResponse 실패 케이스 -> 클라이언트로부터 404 NOT FOUND 응답을 받는 경우를 검증한다.")
void testSendTransactionResponseWithNotFoundFromClient() {
// given
RequestPayload requestPayload = FixtureCreator.getRequestPayload();
ServiceResponseDTOV1 serviceResponse = FixtureCreator.getServiceResponse();
TemplateResponseDTOV1 templateResponse = FixtureCreator.getTemplateResponse();
ZoneFeatureCodeVoV1 zoneFeatureCode = FixtureCreator.getZoneFeatureCode();
when(userTypeResolver.getId(anyInt(), any(), anyString())).thenReturn("validId-123");
when(calculatorClientV1.createStart(anyString(), any())).thenReturn(new ResponseEntity<>(null, HttpStatus.NOT_FOUND));
// when
ResponseEntity<TransactionResponseDTOV1> actualResponse = transactionSender.sendTransactionResponse(requestPayload, serviceResponse, templateResponse, zoneFeatureCode);
// then
assertNotNull(actualResponse);
assertEquals(HttpStatus.NOT_FOUND, actualResponse.getStatusCode());
}
@Test
@DisplayName("sendTransactionResponse 메서드가 null 값을 반환하는 경우를 검증한다.")
void testSendTransactionResponseWithNullResponse() {
// given
RequestPayload requestPayload = FixtureCreator.getRequestPayload();
ServiceResponseDTOV1 serviceResponse = FixtureCreator.getServiceResponse();
TemplateResponseDTOV1 templateResponse = FixtureCreator.getTemplateResponse();
ZoneFeatureCodeVoV1 zoneFeatureCode = FixtureCreator.getZoneFeatureCode();
when(calculatorClientV1.createStart(anyString(), any())).thenReturn(null);
// when & then
assertThrows(NullPointerException.class, () -> {
transactionSender.sendTransactionResponse(requestPayload, serviceResponse, templateResponse, zoneFeatureCode);
});
}
@Test
@DisplayName("sendTransactionResponse 메서드가 잘못된 ID를 반환하는 경우 적절한 예외 처리를 하는지 검증한다.")
void testSendTransactionResponseWithInvalidId() throws Throwable {
// given
RequestPayload requestPayload = FixtureCreator.getRequestPayload();
ServiceResponseDTOV1 serviceResponse = FixtureCreator.getServiceResponse();
TemplateResponseDTOV1 templateResponse = FixtureCreator.getTemplateResponse();
ZoneFeatureCodeVoV1 zoneFeatureCode = FixtureCreator.getZoneFeatureCode();
when(userTypeResolver.getId(anyInt(), any(), anyString())).thenReturn(null);
// when & then
assertThrows(
HandlerException.class, () -> transactionSender.sendTransactionResponse(requestPayload, serviceResponse, templateResponse, zoneFeatureCode));
}
}
@SpyBean
과 @MockBean
의 차이는 모킹 동작과 객체의 실제 동작에 있다.
@SpyBean
@SpyBean
은 실제 객체를 생성하고, 필요한 경우에만 특정 메서드를 모킹한다.- 다른 메서드가 호출되면 실제 구현이 실행된다.
- 상태를 가진 빈에 유용하며, 실제 빈의 일부만을 모킹할 때 사용된다.
@MockBean
@MockBean
은 모든 메서드가 모킹되는 가짜 객체를 생성한다.- 빈의 실제 구현을 사용하지 않고, 모든 메서드가 기본적으로 모킹된다.
- 상태가 없거나, 전체 빈을 모킹할 때 유용하다.
위의 테스트 코드에서 @SpyBean
을 사용하면 CalculatorClientV1
와 UserTypeResolver
클래스의 실제 구현이 호출되지만, when(...).thenReturn(...)
같은 문법을 사용하여 특정 메서드만 모킹할 수 있다.
반면에 @MockBean
을 사용하면 이러한 빈들의 모든 메서드는 기본적으로 모킹되므로, 특정 동작을 지정하지 않으면 null이나 기본값을 반환한다.
'Programming > Java, Spring' 카테고리의 다른 글
스프링부트 junit 테스트 `No tests found for given includes:` 메시지 (0) | 2023.12.17 |
---|---|
Webflux는 얼마나 빠를까? Spring Mvc Vs. Webflux 성능 비교 테스트 (0) | 2023.12.14 |
spring mock mvc에서 한 번쯤은 만나는 unnecessary stubbing을 lenient 옵션으로 해결하기 (0) | 2023.12.13 |
스프링 캐싱 메커니즘에서 @Cacheable과 @Cacheput 차이점은 ? (0) | 2023.12.13 |
Spring Webflux - 배경, 개념 Cpu bound vs I/O Bound, block vs non-block, mvc vs webflux (1) | 2023.10.16 |