[Spring] Mockito에 대해서!

YoungHo-Cha·2021년 10월 3일
1

Testing

목록 보기
5/7

오늘은 Mockito에 대해서 자세히 알아보자.


🚗목차

  • 사용하는 상황
  • Mockito란?
  • 어떻게 사용하나?

🧐사용하는 상황

우리가 협업하는 상황이라고 가정해보자.

🔎Case 1

  • A 개발자는 Repository 개발
  • B 개발자는 Service 개발
  • 나는 Controller를 개발하고 있다.

나는 Controller를 테스트하고 싶다. 근데 A, B 개발자가 개발을 완료하지 않았다.

그래서 테스트를 할 수 없다..

이 때!!! Mockito를 이용하여 가짜 객체를 생성해서 개발이 완료되었다는 전제를 설정해준다!

🔎Case 2

  • 네트워크를 이용하여 API 정보를 받아서 로직을 수행한다고 가정하자.
  • 실제로 존재하는 API를 사용하여 테스트를 진행하면, 테스트는 네트워크 연결에 영향을 받는다.
  • 즉, 나는 로직을 테스트하고 싶은데 해당 네트워크 이슈로 해당 테스트가 올바르게 작동하지 않을 수 있다.
  • 네트워크를 대신하여 작동을 할 수 있는 가짜 객체를 생성한다.

🧐Mockito란?

Mockito는 단위테스트를 도와주는 Java Mocking FrameWork다.

위와 같은 Case를 도와주는 객체이다.


🧐Mockito의 무엇을 알아야 하나?

Mockito로 테스트할 수 있는 요소는 많다.

🔎Test Double

Test Double은 영화 촬영에서 나오는 스턴트 더블(한국은 스턴트 맨)에서 가져온 단어이다.

어떠한 역할(객체)을 대신하여 수행해주는 가짜 객체를 이용하여 테스팅을 수행하는 모든 방법을 "Test Double"이라 한다.

가짜 객체도 Dummy, Fake, Stub, Spy, Mock으로 5가지의 역할이 나뉜다.

🔎Dummy

객체는 전달되지만 사용되지 않는 객체이다.

🔎Fake

실제로 동작하는 코드를 가지고 있는 가짜 객체이다. 하지만 프로덕션의 코드에 사용하기엔 허접한 객체다. 아주 간단한 코드(해당 로직에 필요한 기능)만 가지고 있다.

🔎Stub

가짜 객체를 실제로 동작하는 것처럼 보이게 만드는 개체이다.
테스트에서 호출된 요청에 대해서 미리 준비해둔 결과를 전달한다.

🔎Spy

실제로 생성한 객체 인스턴스에서 부분적으로 mock을 생성하여 원하는 검증을 할 수 있도록 한다.

🔎Mock

주로 Stub와 헷갈리는 객체이다. 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍된 객체이다. 테스트 작성을 위한 환경 구축이 어려울 때, 테스트하고자 하는 코드와 엮인 객체들을 대신하기 위해서 만들어진 객체이다.

Stub vs Mock

차이점을 알기 위해서는 먼저 행위 검증, 상태 검증에 대해서 알아야 한다.

  • 상태 검증 : 메소드를 실행하고, 객체의 상태를 확인하여 올바르게 동작했는지 확인하는 것이다. (예를 들면, 변수의 값이 변경되있는 것을 확인한다.)

  • 행위 검증 : 메소드의 리턴 값으로 판단할 수 없는 경우이다. 특정한 동작이 수행되었는지 검증하는 것이다.

상태 검증 예시 코드

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를 통해서 값을 검증하지 않는다.
그냥 해당하는 동작이 수행되었는지 확인하는 것이다.


🧐어떻게 사용하니?

🔎객체 생성

  1. Mocito.mock(생성 클래스)
	
    SampleA sample = Mockito.mock(SampleA.class);

이렇게 하면 "SampleA.class"가 가짜로 생성된다.

  1. @Mock

@Mock
private SampleA sample;

어노테이션 "@Mock"을 생성하려는 객체에 달아주면, Mock을 자동으로 생성해준다.

  1. InjectMocks

생성되는 Mock을 특정 객체에 주입한다.

예시 코드로 보자.

나는 Service를 테스트 하고 싶다.

Service 클래스에는 구현되지 않은 Repository 객체가 존재할 것이다..
만약 테스트에서 내가 Service를 테스트 하려면?

Repository가 있어야 한다. 그래서 Repository를 가짜 객체로 생성해주자!


@SpringBootTest
public class ProductServiceTest {

    @Mock
    private ProductRepository productRepository;

    @InjectMocks
    private ProductService productService;

};

위의 코드를 보면, "@Mock"을 생성하고 "@InjectMocks"를 이용하여 "productService"에 "Repository"를 주입해준 것을 볼 수 있다.

  1. @ExtendWith(MockitoExtension.class)

위의 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 되도록 할 수 있다.

🔎Verification

검증단계이다.

아까 언급했던 상태검증, 행위검증이 존재한다.

상태검증 부터 살펴보자.

상태검증

@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는 내 생각에 행위검증에서 정말 사용하기 좋은 것 같다.

  1. mock 객체 생성
Product mockProduct = mock(Product.class);
  1. 생성된 객체의 메서드 호출
mockProduct.setPrice("10");
  1. 행위 검증
  • verify( "생성한 mock 객체" ). 검증할메서드;
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("과자"); // 행위검증 가능
  1. 호출횟수 및 호출시간 검증

횟수 - time(), 시간 - timeout()

  • 호출 횟수 검증
    verify("생성한 객체","횟수" )."호출메서드";

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");
  1. 순서 검증
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");


📋마치며

아직 풀리지 않은 미스테리가 있다.

  • "@Mock"으로 mock을 생성하는 경우는 상태 검증만을 위한 방법인가?
  • mock(클래스) 로 생성하는 mock에 InjectMocks가 필요없는건가?

차츰 차츰 글을 수정해야겠다!


🧷Reference

profile
관심많은 영호입니다. 궁금한 거 있으시면 다음 익명 카톡으로 말씀해주시면 가능한 도와드리겠습니다! https://open.kakao.com/o/sE6T84kf

0개의 댓글