Mokito @Mock , @MockBean , @Spy , @SpyBean
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은 실제 구현된 객체를 감싸는 프록시 객체 형태이기 때문에 스프링 컨텍스트에 실제 구현체가 등록되어 있어야 합니다.