오늘은 Mockito에 대해서 자세히 알아보자.
우리가 협업하는 상황이라고 가정해보자.
나는 Controller를 테스트하고 싶다. 근데 A, B 개발자가 개발을 완료하지 않았다.
그래서 테스트를 할 수 없다..
이 때!!! Mockito를 이용하여 가짜 객체를 생성해서 개발이 완료되었다는 전제를 설정해준다!
Mockito는 단위테스트를 도와주는 Java Mocking FrameWork다.
위와 같은 Case를 도와주는 객체이다.
Mockito로 테스트할 수 있는 요소는 많다.
Test Double은 영화 촬영에서 나오는 스턴트 더블(한국은 스턴트 맨)에서 가져온 단어이다.
어떠한 역할(객체)을 대신하여 수행해주는 가짜 객체를 이용하여 테스팅을 수행하는 모든 방법을 "Test Double"이라 한다.
가짜 객체도 Dummy, Fake, Stub, Spy, Mock으로 5가지의 역할이 나뉜다.
객체는 전달되지만 사용되지 않는 객체이다.
실제로 동작하는 코드를 가지고 있는 가짜 객체이다. 하지만 프로덕션의 코드에 사용하기엔 허접한 객체다. 아주 간단한 코드(해당 로직에 필요한 기능)만 가지고 있다.
가짜 객체를 실제로 동작하는 것처럼 보이게 만드는 개체이다.
테스트에서 호출된 요청에 대해서 미리 준비해둔 결과를 전달한다.
실제로 생성한 객체 인스턴스에서 부분적으로 mock을 생성하여 원하는 검증을 할 수 있도록 한다.
주로 Stub와 헷갈리는 객체이다. 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍된 객체이다. 테스트 작성을 위한 환경 구축이 어려울 때, 테스트하고자 하는 코드와 엮인 객체들을 대신하기 위해서 만들어진 객체이다.
차이점을 알기 위해서는 먼저 행위 검증, 상태 검증에 대해서 알아야 한다.
상태 검증 : 메소드를 실행하고, 객체의 상태를 확인하여 올바르게 동작했는지 확인하는 것이다. (예를 들면, 변수의 값이 변경되있는 것을 확인한다.)
행위 검증 : 메소드의 리턴 값으로 판단할 수 없는 경우이다. 특정한 동작이 수행되었는지 검증하는 것이다.
상태 검증 예시 코드
public interface MailService {
public void send (Message msg);
}
// 메일 서비스를 테스트 하기 위해 추가적으로 메소드 만들어짐
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
class OrderStateTester...
public void testOrderSendsMailIfUnfilled() { // 상태 검증
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent()); // 올바른 상태 값이 나오는지 체크(테스트)
}
MailService를 검증하기 위해서, MailServiceStub를 생성해서 단순한 로직을 만들어 놓은 것을 볼 수 있다.
행위 검증
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
}
위의 Stub와는 다르게 Mock은 Equal를 통해서 값을 검증하지 않는다.
그냥 해당하는 동작이 수행되었는지 확인하는 것이다.
SampleA sample = Mockito.mock(SampleA.class);
이렇게 하면 "SampleA.class"가 가짜로 생성된다.
@Mock
private SampleA sample;
어노테이션 "@Mock"을 생성하려는 객체에 달아주면, Mock을 자동으로 생성해준다.
생성되는 Mock을 특정 객체에 주입한다.
예시 코드로 보자.
나는 Service를 테스트 하고 싶다.
Service 클래스에는 구현되지 않은 Repository 객체가 존재할 것이다..
만약 테스트에서 내가 Service를 테스트 하려면?
Repository가 있어야 한다. 그래서 Repository를 가짜 객체로 생성해주자!
@SpringBootTest
public class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
};
위의 코드를 보면, "@Mock"을 생성하고 "@InjectMocks"를 이용하여 "productService"에 "Repository"를 주입해준 것을 볼 수 있다.
위의 Mock 객체도 "@ExtendWith"설정을 해주지 않으면 생성되지 않는다.
@ExtendWith(MockitoExtension.class)
public class test1{
@Mock
private SampleA sample;
}
✨ "@ExtendWith"는 해당 테스트 클래스 전체에 적용해 줄 기능이 있을 때 사용(= JUnit4 - RunWith)
@ExtendWith를 이용하지 않고 initMocks으로 설정 가능하다.
public class test1{
@Mock
private SampleA sample;
@Before
public void initMocks(){
MockitoAnnotation.initMocks(this);
}
}
해당 설정을 해주면 Mockito 문법 오류 예측과 같은 기능을 추가적으로 제공한다.
동작을 정의하는 것은 Stubbing이라고 불린다.
다음 코드를 확인하자.
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
public void productServiceFindByDataTypeMethodTest(){
//given
Product product = new Product();
product.setTitle("과자");
product.setPrice("1000");
List<Product> productList = new ArrayList<>();
productList.add(product);
given(productService.findByDataType("과자")).willReturn(productList); // 1번
when(productService.findByDataType("과자").thenReturn(productList); // 2번
}
}
1번, 2번 둘 다 가능하다.
productService의 findByDataType("과자")가 호출이 되면 productList가 return 되는 것이라고 설정한 것이다.
✨ thenReturn, willReturn을 이어 붙여 여러개가 Return 되도록 할 수 있다.
검증단계이다.
아까 언급했던 상태검증, 행위검증이 존재한다.
상태검증 부터 살펴보자.
상태검증
@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock //mock 생성
private ProductRepository productRepository;
@InjectMocks //mock 주입
private ProductService productService;
@Test //test 시작
public void productServiceFindByDataTypeMethodTest(){
//given
Product product = new Product();
product.setTitle("과자");
product.setPrice("1000");
List<Product> productList = new ArrayList<>();
productList.add(product);
given(productService.findByDataType("과자")).willReturn(productList);
//when
List<Product> findProductList = productService.findByDataType("과자");
//then
Assertions.assertEquals(productList, findProductList);
Assertions.assertEquals(productList.get(0), findProductList.get(0));
}
};
when 단계에서 productService의 리턴 값을 받고, then에서 상태 검증을 한 것을 볼 수 있다.
행위검증(Spying)
mockito는 내 생각에 행위검증에서 정말 사용하기 좋은 것 같다.
Product mockProduct = mock(Product.class);
mockProduct.setPrice("10");
verify(mockProduct).setPrice("10");
위의 1, 2, 3 을 입력해주고 테스트를 하면 테스트가 통과된다.
🤔 그럼 상태검증에서 사용한 객체를 행위검증 할 수 없나? 생각이 들었다. 그래서 다음과 같이 코드를 입력하고 동작시켜보았다.
@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
public void productServiceFindByDataTypeMethodTest(){
Product pp1 = mock(Product.class);
pp1.setPrice("1");
verify(pp1).setPrice("1");
//given
Product product = new Product();
product.setTitle("과자");
product.setImagePath("C://");
product.setLink("http://");
product.setImageName("과자사진");
product.setDataType("과자");
product.setPrice("1000");
List<Product> productList = new ArrayList<>();
productList.add(product);
given(productService.findByDataType("과자")).willReturn(productList);
//when
List<Product> findProductList = productService.findByDataType("과자");
//then
Assertions.assertEquals(productList, findProductList);
Assertions.assertEquals(productList.get(0), findProductList.get(0));
verify(productService).findByDataType("과자");
verify(productRepository).findByDataType("과자");
}
}
안대누..
mock(클래스)로 생성해야 행위 검증이 가능한가보다.
반대로는 가능하다.
ProductService mockProductService = mock(ProductService.class);
when(mockProductService.findByDataType("과자")).thenReturn("ok");
String result = mockProductService.findByDataType("과자");
Assertions.assertEquals("ok", result); // 상태검증 가능
verify(mockProductService).findByDataType("과자"); // 행위검증 가능
횟수 - time(), 시간 - timeout()
time(1)이 기본값이다.
//2번 호출인지 검증
verify(mockProduct, times(2)).setPrice("10");
verify("생성한 객체", "시간"."호출메서드";
// 100 마이크로초 이하인지 검증
verify(mockProduct, timeout(100)).setPrice("10");
verify(mockProduct, timeout(100).times(2)).setPrice("10");
Product mockProduct = mock(Product.class);
mockProduct.setPrice("1");
mockProduct.setPrice("2");
// 순서 검증 객체에 등록
InOrder inOrder = inOrder(mockProduct);
// 순서 검증
inOrder.vericy(mockProduct).setPrice("1");
inOrder.vericy(mockProduct).setPrice("2");
아직 풀리지 않은 미스테리가 있다.
차츰 차츰 글을 수정해야겠다!