Spring boot/기본 정리

Mokito @Mock , @MockBean , @Spy , @SpyBean

코딩딩코 2022. 12. 19. 01:29

Test Double - Mokito

스프링과 Junit을 이용해서 테스트 코드를 작성하다 보면 테스트 환경(database, api)을 구현하는 코드까지 작성해야 하고 실제 테스트할 코드보다 환경을 구현하는 코드가 훨씬 더 복잡해지게 됩니다.

이런 문제 영역을 해결하기 위해서 Test Double이라는 것이 나왔고 Java에서는 대표적으로 Mockito가 있습니다.

 

Mokito 어노테이션에는

@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks가 존재합니다.

 

Test Double을 사용하는 이유

@RequiredArgsConstructor
@Service
public class ProductService {

    private final ProductRepository productRepository;
    private final AlertService alertService;

    public void createProduct(Product productInfo) throws Exception {
        Product product = productRepository.findProduct(productInfo);
        if(product != null){
            throw new AlreadyHasProductException();

        }else {
            boolean result = productRepository.createProduct(productInfo) > 0;

            if(result) {
                alertService.successAlert();

            }else {
                alertService.failedAlert();

            }
        }

    }
}

상품을 등록한다는 예시로 해보겠습니다.

dto를 받아서 Entity로 변환시킨 후 Repository에 넘겨서 해당 상품이 존재하는지 파악합니다.

해당 상품이 이미 존재한다면 AlreadyHasProductException 예외를 호출합니다.

존재하지 않는 새로운 상품이라면 INSERT를 진행하고 그 결과에 따라 alertService의 메소드를 호출합니다.

이러한 코드를 테스트를 하려고 한다면

 

class Test {
    private ProductService productService;

    @Test
    void createOrderTest() {
        ProductRepository productRepository = new ProductRepository();
        AlertService alertService = new AlertService();

        productService = new ProductService(productRepository, alertService);
        Product product = new Product();

        productService.createProduct(product);
    }

}

이런 식으로 테스트 하기 위해서 준비되어야 하는 로직이 너무 많습니다.

만약에 여기서 의존관계가 더 늘어난다면 더 복잡한 로직이 되어 버립니다.

여기서 우리는 실제 값이 들어가는지 테스트 하는 것이 아닌 예외가 잘 던져지는지 결과 값이 잘 나오는지가 관심사입니다.

 

이렇듯 메소드의 실제 내부 동작은 실행되지 않고 상황 설정만 할 수 있도록 해결한 것이 Test Double입니다.

 

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    private ProductService productService;

    @Test
    @DisplayName("successAlert 1번 호출")
    void createProductTest() throws Exception {
    	// mock()을 이용해서 가짜 객체를 생성
        ProductRepository productRepository = mock(ProductRepository.class);
        AlertService alertService = mock(AlertService.class);
        
        // 가짜 객체를 주입
        productService = new ProductService(productRepository, alertService);
        Product product = new Product();

        // 반환값을 임의로 지정
        when(productRepository.findProduct(product)).thenReturn(null);
        when(productRepository.createProduct(product)).thenReturn(1);

        productService.createProduct(product);

        // 1번 호출 되었는지 확인
        verify(alertService, times(1)).successAlert();
    }
}

    	....

        // 반환값을 임의로 지정
        when(productRepository.findProduct(product)).thenReturn(null);
        
        // 0을 반환한다면
        when(productRepository.createProduct(product)).thenReturn(0);

        productService.createProduct(product);

        // 1번 호출 되었는지 확인
        verify(alertService, times(1)).successAlert();
    }
}

 

반환이 0이라면 result는 false가 되어 alertService의 failedAlert() 메소드가 호출되어 verify에서 같지 않기 때문에

실패했다고 나옵니다. successAlert()이 호출 되지 않았지만 failedAlert()이 1번 호출되었다고 나오네요.

    	....

        // 반환값을 임의로 지정
        when(productRepository.findProduct(product)).thenReturn(null);
        
        // 0을 반환한다면
        when(productRepository.createProduct(product)).thenReturn(0);

        productService.createProduct(product);

        // 1번 호출 되었는지 확인
        verify(alertService, times(1)).failedAlert();
    }
}

failedAlert()이 1번 호출되었는지 검증을 하니까 테스트 통과되었습니다.

 

    	....

        // 반환값을 임의로 지정
        when(productRepository.findProduct(product)).thenReturn(new Product());
        
        // 0을 반환한다면
        when(productRepository.createProduct(product)).thenReturn(0);

        productService.createProduct(product);

        // 1번 호출 되었는지 확인
        verify(alertService, times(1)).failedAlert();
    }
}

이번엔 findProduct()메소드에서 DB에서 데이터를 확인한 결과 값이 있어서 객체를 반환했다면

Exception 호출이 되게끔 구현이 잘 되는지 확인을 해봤습니다.

@Mock

@Mock으로 만든 mock 객체는 가짜 객체이며 그 안에 메소드 호출해서 사용하려면 반드시 스터빙(stubbing) 해야 합니다. 

스터빙을 하지 않고 호출한다면 primitive type은 0, 참조형은 null을 반환합니다.

 

(Mokito.mock()으로 생성한 가짜 객체도 동일합니다.)

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    // 가짜 객체를 @Mock 어노테이션으로 선언

    @Mock
    private ProductRepository productRepository;
    @Mock
    private AlertService alertService;

    private ProductService productService;

    @Test
    @DisplayName("successAlert() 호출")
    void createProductTest() throws Exception {
        // 가짜 객체를 주입
        productService = new ProductService(productRepository, alertService);
        Product product = new Product();

        when(productRepository.findProduct(product)).thenReturn(null);
        when(productRepository.createProduct(product)).thenReturn(1);

        productService.createProduct(product);

        verify(alertService, times(1)).successAlert();
    }
}

 

Stubbing(스터빙)

만들어진 mock 객체의 메소드를 실행했을 때 리턴 값을 임의로 정의하는 것이라고 생각하시면 됩니다.

 

thenReturn: 스터빙한 메소드 호출 후 어떤 객체를 리턴할 건지 정의

thenThrow: 스터빙한 메소드 호출 후 어떤 Exception을 Throw 할 건지 정의

thenAnswer: 스터빙한 메소드 호출 후 어떤 작업을 할지 custom하게 정의

thenCallRealMethod: 실제 메소드 호출

 

 

@MockBean

 

@MockBean은 스프링 컨텍스트에 mock객체를 등록하게 되고 스프링 컨텍스트에 의해 @Autowired가 동작할 때 등록된 mock객체를 사용할 수 있도록 동작합니다.

 

@Autowired라는 어노테이션으로 컨텍스트에서 알아서 생성된 객체를 주입받아 제어의 역전이 적용되어 테스트를 진행할 수 있도록 합니다.

 

@Mock @MockBean DI 차이점

@Mock은 @InjectMocks에 대해서만 해당 클래스 안에서 정의된 객체를 찾아서 의존성을 해결합니다. (또는 직접 생성자에게 인자로 넘겨주는 방법입니다.)

@MockBean은 mock 객체를 스프링 컨텍스트에 등록하는 것이기 때문에 @SpringBootTest를 통해서 @Autowired에 의존성이 주입되게 됩니다.

 

@Spy @SpyBean의 차이점도 위와 같습니다.

 

 

@InjectMocks

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    @Mock
    private ProductRepository productRepository;
    @Mock
    private AlertService alertService;
    @InjectMocks
    private ProductService productService;

    @Test
    @DisplayName("successAlert() 호출")
    void createProductTest() throws Exception {
    	// 객체 주입을 직접 해주지 않아도 됩니다.
        // productService = new ProductService(productRepository, alertService);
        Product product = new Product();

        when(productRepository.findProduct(product)).thenReturn(null);
        when(productRepository.createProduct(product)).thenReturn(1);

        productService.createProduct(product);

        verify(alertService, times(1)).successAlert();
    }
}

@InjectMocks는 DI를 @Mock이나 @Spy로 생성된 mock 객체를 @Autowired와 같이 자동으로 주입해주는 어노테이션입니다.

 

 

@Spy

@Spy로 만든 mock 객체는 진짜 객체이며 메소드 실행 시 스터빙을 하지 않으면 기존 객체의 로직을 실행한 값을 반환하고, 스터빙을 한 경우엔 스터빙 값리턴합니다.

 

하나의 객체를 선택적으로 stub할 수 있도록 하는 기능이 있는데 @Spy입니다.

 

 

@SpyBean

 

@MockBean과 마찬가지로 스프링 컨테이너에 Bean으로 등록된 객체에 대해 Spy를 생성합니다.

@SpyBean은 @Spy의 특성을 가진 채로 @MockBean처럼 @Autowired로 자동 주입 가능하게 합니다.

 

 

@SpyBean 사용 시 주의해야 할 점

 

@SpyBean이 Interface일 경우에는 해당 Interface를 구현하는 실제 구현체가 꼭 스프링 컨텍스트에 등록되어 있어야 합니다.

@SpyBean은 실제 구현된 객체를 감싸는 프록시 객체 형태이기 때문에 스프링 컨텍스트에 실제 구현체가 등록되어 있어야 합니다.