ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • @SpyBean, @MockBean
    Today I Learned 2022. 10. 8. 23:59

     

    그동안 Spring 백엔드 애플리케이션을 구현하면서 Controller를 테스트할 때 왜 어떤 상황에서는 @SpyBean을 쓰고 어떤 상황에서는 @MockBean을 쓰는 것인지 구체적으로 알지 못한 채 사용하고 있었다.

     

    JpaRepository처럼 실제로 인스턴스를 만들어 사용할 수 없는 것이면 @MockBean을 붙이고, 나머지는 모두 @SpyBean을 붙인 뒤에 테스트하는 건가 보다... 정도의 느낌으로 사용하고 있었는데, 의외의 지점에서 혹시 이건가? 싶은 영감이 들었다. 바로 Jest에서 지원하는 함수 중 하나인 jest.spyOn()을 사용해보면서였다.

     

    다음 소스코드 하단부의 테스트 코드에는 changeReceiverInput 함수가 실행되었을 때 publish 함수가 실제로 실행되었는지를 검증하는 과정이 포함되어 있다.

     

    // Implementation
    // OrderStore.js
    export default class OrderStore extends Store {
      constructor() {
        this.receiver = '';
        this.listeners = new Set();
      }
    
      changeReceiverInput(receiver) {
        this.receiver = receiver;
        this.publish();
      }
    
      subscribe(listener) {
        this.listeners.add(listener);
      }
    
      unsubscribe(listener) {
        this.listeners.delete(listener);
      }
    
      publish() {
        this.listeners.forEach((listener) => listener());
      }
    }
    
    export const store = new Store();
    
    
    // Test
    // OrderStore.test.js
    context('입력 필드의 내용을 수정할 경우', () => {
      const orderStore = new OrderStore();
    
      const spyPublish = jest.spyOn(orderStore, 'publish');
    
      orderStore.changeReceiverInput('치코리타');
    
      it('입력 필드 상태를 정상적으로 수정하고 publish를 수행', () => {
        expect(orderStore.receiver).toBe('치코리타');
        expect(spyPublish).toBeCalled();
      });
    });

     

    spyOn()은 mock()과는 달리 어떤 사전 설정을 해주는 과정이 없었다. 이름에 들어가는 'spy'의 의미 그대로 특정 클래스 내에서 지정해준 이름과 같은 대상을 찾아 그 동작을 추적하고 있었다. 이때 그 대상은 mock처럼 모의로 정의된 대상이 아니라, 테스트 코드 내 동작을 수행하는 과정에서 영향받거나 호출되는 실제 대상이라는 점이 눈에 들어왔다.

     

     

    그렇다면 Spring에서 Controller를 테스트할 때 @SpyBean으로 지정해주는 대상들은 테스트 코드를 실행시킬 때 해당 대상이 실제로 Spring Container에서 관리되는 Bean 객체임을 알려주기 위한 것이고, @MockBean으로 지정해주는 것들도 실체가 없는 껍데기만 있는 Bean 객체인 것이라고 해놓은 것이기 때문에 given().willReturn()이나 given().willThrow()로 직접 어떤 동작들을 정의해줘야 하는 것인가? 싶었다. 세운 가정이 맞는지 확인하기 위해 자료들을 조사했다.

     

    Spring 공식 문서에서 제공하는 @MockBean과 @SpyBean의 정의는 다음과 같다.

     

     

    @SpyBean

    Annotation that can be used to apply Mockito spies to a Spring ApplicationContext. Can be used as a class level annotation or on fields in either @Configuration classes, or test classes that are @RunWith the SpringRunner.

    Spies can be applied by type or by bean name. All beans in the context of a matching type (including subclasses) will be wrapped with the spy. If no existing bean is defined a new one will be added. Dependencies that are known to the application context but are not beans (such as those registered directly) will not be found and a spied bean will be added to the context alongside the existing dependency.

     

     

    All beans in the context of a matching type (including subclasses) will be wrapped with the spy. 라는 내용으로부터 @SpyBean이 실제로 등록된 Bean 객체들을 spy로 랩핑한 채 사용하도록 하는 것임을 확인할 수 있었다. 

     

    해당 부분을 확인하고 나니 다른 블로그에서 @SpyBean의 한 가지 특징적인 점을 설명하는 부분이 눈에 들어왔다. @SpyBean 어노테이션이 할당된 객체들은 실제 구현된 내용대로 테스트 코드 상에서 작동하지만, 특정 메서드는 Mocking해 사용하는 것도 가능하다는 부분이었다.

     

     

     

     

    @MockBean

    Annotation that can be used to add mocks to a Spring ApplicationContext. Can be used as a class level annotation or on fields in either @Configuration classes, or test classes that are @RunWith the SpringRunner.

    Mocks can be registered by type or by bean name. When registered by type, any existing single bean of a matching type (including subclasses) in the context will be replaced by the mock. When registered by name, an existing bean can be specifically targeted for replacement by a mock. In either case, if no existing bean is defined a new one will be added. Dependencies that are known to the application context but are not beans (such as those registered directly) will not be found and a mocked bean will be added to the context alongside the existing dependency.

     

     

    When registered by type, any existing single bean of a matching type (including subclasses) in the context will be replaced by the mock. 이라는 구문을 통해 실제로 등록된 Bean 객체를 사용하지 않고 mock으로 대체한다는 점을 확인할 수 있었다.

     

    그렇기 때문에 JavaScript에서 jest.mock()으로 Mocking한 대상의 데이터나 동작들은 직접적인 값을 주거나 모의로 정의해줬던 것처럼 Java에서도 given()과 then 계열 메서드로 동작을 정의해줘야 했던 것이구나라는 점을 좀 더 구체적으로 인지할 수 있었다.

     

     

     

    References

    - https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/MockBean.html

    - https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/SpyBean.html

    - https://jojoldu.tistory.com/226

    - https://cobbybb.tistory.com/16

     

     

     

    댓글

Designed by Tistory.