본문 바로가기
Programming/Java, Spring

spring 단위 테스트에서 autowired와 mockbean의 차이 (feat. spybean)

by Renechoi 2023. 12. 13.

단위 테스트를 작성하면서 쓰긴 쓰는데 한참 동안 차이점도 잘 모르고 썼던 것이 바로 @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 빈을 사용하게 된다.
  • ClientInterfaceV1UserTypeResolver@MockBean으로 주입되므로, 이들은 모킹된 객체가 되어 실제 로직을 수행하지 않는다.

 

한 마디로 실제 객체와 mock 객체의 차이다.

 

TestConfig 클래스에서 ClientInterfaceV1 빈은 명시적으로 Mock 객체를 리턴하도록 설정되어 있지만, 테스트 클래스에서 @MockBean을 사용하면 이 설정은 무시되고 새로운 Mock 객체가 생성된다. 따라서 @MockBean은 테스트 중에 동적으로 빈을 교체하는 용도로 사용될 수 있다.

 

위 코드의 예를 살펴보면 Feign 요청을 처리해야 하는데 만약 Client를 실제로 하면 어떻게 될까? 테스트 코드가 돌아갈 때마다 실제 api 호출이 발생한다. 즉, 특정 서버의 테스트를 위해서 다른 서버의 실제 자원이 사용되어야 하는 것이다. 이러한 의존성은 당연히 바람직하지 못하다. 이렇게 실제 다른 서비스와의 의존성을 배제하기 위해 Clientmock으로 대체하는 것이다.

 

다음과 같이 정리해보자.

 

 


질문: 왜 @MockBean을 사용하나?

 

  • @MockBean은 실제 빈의 복잡한 로직이나 외부 시스템의 의존성 없이, 특정 빈의 동작을 테스트하는 데에 주로 사용된다.
  • 예를 들어, ExternalApiClientV1의 경우 실제 외부 API 호출을 하지 않으면서도 API 호출의 결과를 시뮬레이션하고 싶을 때 유용하다.
  • @MockBean을 사용하면, 테스트 중에 해당 빈의 행동을 동적으로 조작할 수 있으며, 다양한 시나리오를 테스트하기 용이하다.

 

질문: @Autowired@MockBean을 함께 사용하는 이유는 무엇인가?

  • @Autowired@MockBean을 함께 사용함으로써, 테스트 대상 클래스(GenericServiceFeignSender)는 실제 환경에서와 같이 작동하게 하면서, 일부 의존성(예: ExternalApiClientV1, UserTypeResolver)은 테스트용 모킹 객체로 대체할 수 있다.
  • 이렇게 하면, 테스트 대상 클래스가 실제 환경에 가깝게 동작하면서도, 외부 의존성에 대한 복잡성을 줄이고, 테스트의 범위를 좁혀 집중적인 테스팅이 가능해지는 장점이 있다.

 

예를 들면...GenericServiceFeignSender 클래스가 ExternalApiClientV1UserTypeResolver에 의존하고 있다고 가정해보자 이러한 의존성들이 실제 외부 서비스와 연결되어 복잡한 로직을 포함하고 있다면, 단위 테스트 작성 시 실제 외부 서비스와의 상호작용이나 로직의 복잡성을 모의(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를 통해 주입되며, ExternalApiClientV1UserTypeResolver@MockBean을 통해 모의 객체로 대체된다. 이를 통해 ExternalApiClientV1에서의 외부 API 호출과 UserTypeResolver의 사용자 타입 결정 로직을 테스트 내에서 제어할 수 있게 된다.

 


 

그런데 Spybean이라는 것이 또 있다 !

 

두둥...!

 

https://rhodes2safety.com/canine-tip-of-the-day-shock/

 

 

다음과 같은 코드에서 그렇다면 spybeanmockbean의 차이는 또 무엇일까 ?!

 

간단히만 살펴보자.

 

@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을 사용하면 CalculatorClientV1UserTypeResolver 클래스의 실제 구현이 호출되지만, when(...).thenReturn(...) 같은 문법을 사용하여 특정 메서드만 모킹할 수 있다.

 

반면에 @MockBean을 사용하면 이러한 빈들의 모든 메서드는 기본적으로 모킹되므로, 특정 동작을 지정하지 않으면 null이나 기본값을 반환한다.

반응형