본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android] Mockito 맛보기 ( test library )

by 돼지왕왕돼지 2018. 12. 7.
반응형


https://www.tutorialspoint.com/mockito/mockito_overview.htm

http://www.vogella.com/tutorials/Mockito/article.html

https://static.javadoc.io/org.mockito/mockito-core/2.12.0/org/mockito/Mockito.html#mockito


-

Mockito 는 JUnit 위에서 동작하며 Mocking 과 Verification, Stubbing 을 도와주는 프레임워크이다. ( 이 자체가 testing 하는 framework 는 아니다!! )

Mockito 를 사용하면 Mock 을 만들어서 external dependency 를 제거할 수 있고, code 가 제대로 수행하는지 검증할 수도 있다.



-

unit testing 의 목표 중 하나는 test 가 독립적이어야 한다는 것이다.

다른 class 에 side effect 를 주면 안 된다.

이것은 real dependency 가 아닌 test replacement(test double)을 통해 할 수 있으며, test doubles 는 아래와 같이 구분될 수 있다.


1. dummy object : method 는 전혀 사용되지 않을 dummy object 가 전달된다.

2. fake object : working impl 을 가진 녀석인데 보통 간소화되어있다. 예를 들면 real db 가 아닌 memory db 를 참조해 결과를 return 한다던지

3. stub : stub 은 test 를 위해 사용되기 위한 부분 구현된 녀석이다. 보통 이 녀석은 외부 호출에 대해 아무 반응하지 않고, log 만 쌓는다던지 그런 역할만 한다.

4. mock object : dummy 구현을 가진 녀석으로, 구현 없이 output 만 내어준다. 


test double 은 test 당하는 object 에 전달되어질 수 있다.



-

Mock interface 를 통해 실제 database connection, property file, server 연결 등이 필요 없게 된다. mock 이 dummy data 를 제공해주어 이것들을 대신해준다. Proxy 라고 보면 된다.

Mocking 과정은 reflection 을 이용해서 된다. 따라서 test 를 위한 mock class 를 생성할 필요가 없다는 것이 최대 장점 중 하나이다.



-

Mockito version 2.6.1 부터 AndroidTest 로 support 가능해졌다.

core 만으로 일반 VM 에서 test 하는 것도 당연 가능하다.

repositories { 
    jcenter() 
} 

dependencies { 
    …
    testCompile “jnuit:jnuit:+”
    testCompile "org.mockito:mockito-core:+” 
    androidTestCompile "org.mockito:mockito-android:+” 
}



-

JUnit 과 Mockito integration

// Mock 이 될 Interface
public interface CalculatorService {
   public double add(double input1, double input2);
   public double subtract(double input1, double input2);
   public double multiply(double input1, double input2);
   public double divide(double input1, double input2);
}



// 테스트 대상
public class MathApplication {
   private CalculatorService calcService;

   // 주입 받는다.
   public void setCalculatorService(CalculatorService calcService){
      this.calcService = calcService;
   }

   public double add(double input1, double input2){
      return calcService.add(input1, input2);
   }
   
   public double subtract(double input1, double input2){
      return calcService.subtract(input1, input2);
   }
   
   public double multiply(double input1, double input2){
      return calcService.multiply(input1, input2);
   }
   
   public double divide(double input1, double input2){
      return calcService.divide(input1, input2);
   }
}


// Test 대상을 test 하기 위한 class
// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
       
   //@InjectMocks annotation is used to create and inject the mock object
   @InjectMocks 
   MathApplication mathApplication = new MathApplication();

   //@Mock annotation is used to create the mock object to be injected
   @Mock
   CalculatorService calcService;

   @Test
   public void testAdd(){
      //add the behavior of calc service to add two numbers
      when(calcService.add(10.0,20.0)).thenReturn(30.00);
                
      //test the add functionality
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
   }
}

public class TestRunner {
   public static void main(String[] args) {
      Result result = JUnitCore.runClasses(MathApplicationTester.class);
      
      for (Failure failure : result.getFailures()) {
         System.out.println(failure.toString());
      }
      
      System.out.println(result.wasSuccessful());
   }
} 



-

mockito 를 사용할 때는 org.mockito.Mockito.*; 로 static import 를 하는 것이 추천된다.

그렇지 않으면 함수 호출시마다 Mockito.mock() 과 같이 class 이름을 계속 붙여주어야 한다.



-

mock 을 만들 때는 mock(CalculatorService.class) 와 같이 mock 함수를 쓰면 된다.

@Mock annotation 을 사용해도 된다. 이를 사용할 때는 아래와 같이 MockitoRule 을 만들어야 @Mock annotation 이 붙어 있는 녀석을 자동으로. mock 으로 만든다.


@Rule
public MokitoRule mockitoRule = MockitoJUnit.rule();


아니면 test class 자체에 @RunWith(MokitoJUnitRunner.class) 를 마킹해줌으로 갈음할 수 있다.

(위 예제는 이 방법을 사용하였다.)


혹은 MockitoAnnotations.initMocks(testClass); 이 녀석은 test base class 에 넣어두면 좋다.



-

when(calcService.add(10.0,20.0)).thenReturn(30.00);


아래와 같은 syntax 로 작동한다.

when(mock 에 대한 operation(behavior)).thenReturn(returnValue);


위와 같이 코드를 작성하면 mock 에 대해 정의한 operation(behavior) 이 수행되면 thenReturn 에 정의한 return value 가 전달된다. 이를 behavior recording 이라고 부른다.

calcService 를 mocking 한 것이다.


mock 에서 return 이 있는 것들은 기본적으로 null, primitive wrapping, empty collection 이 return 된다.



-

when 문을 통해 behavior 를 추가할 때 특정값이 아닌 특정 형태를 받고 싶을 수 있다.

이럴 때는 anyInt(), anyString(), isNull(), 또는 isA(SpecificClass.class) 와 같은 것을 쓸 수 있다.

( 참고로 isA 는 null 을 return 하기 떄문에 kotlin 에서 nonnull 을 인자로 받는 경우 변형된 형태로 써야 한다. )

또한 specific 한 matching 을 쓰고싶다면 argThat(matcherObj) 혹은 argThat(predicate) 등을 사용할 수도 있다. 

( argThat 도 마찬가지로 null return 한다. )

Comparable<Integer> c = mock(Comparable.class);
when(c.compareTo(anyInt())).thenReturn(-1);
assertEquals(-1, c.compareTo(9));

Comparable<TestClass> c2 = mock(ComparableClass.class)
when(c2.comapreTo(isA(TestClass.class))).thenReturn(0);
assertEquals(0, c.compareTo(new TestClass()));



-

when 문은 thenReturn 대신 thenThrow 도 연결할 수 있다.

Properties prop = mock(Properties.class);
when(prop.get(“Anddroid”)).thenThrow(new IllegalArgumentException(…));
try{
    prop.get(“Anddroid”);
    fail(‘Anddroid is misspelled”);
}catch(IllegalArgumentException ex){
    // good
}


-

아래와 같이 똑같은 호출에도 다른 결과를 return 할 수 있다.

when(mock.someMethod(“some arg”))
    .thenThrow(new RuntimeException())
    .thenReturn(“foo”)
// 첫번째 call 은 exception 이 던져지고, 두번째 call 부터는 foo 가 return 된다.

when(mock.someMethod(“some arg”))
    .thenReturn(“one”, “two”, “three”);
// 첫번째 call 은 one, 두번째 call 은 two, 세번째 call 부터는 three 가 return 된다.


chaining 없이 다시 똑같은 코드를 쓰면서 return 값만 다르게 하거나 하면, overriding 이 된다. 추가가 아니라.
따라서 위의 코드를 수행하면 마지막 thenReturn 이 앞의 thenThrow, thenReturn 을 덮어쓴다.






-

when thenReturn 과 when thenThrow 와 비슷한 doReturn when 과 doThrow when 이 있다.

when thenXXX 은 mock 과 작업할 때 사용하고, doXXX when 은 spy 와 함께 사용할 때 사용한다.

spy 는 나중에 나오지만, mock 이 아닌 진짜 object 를 상대로 api 호출하도록 할 때 사용되는 녀석이다.


do 시리즈를 사용하는 또 다른 이유는 compiler 가 void method 를 bracket 안에 사용하는 것을 좋아하지 않기 때문이다.

그래서 void method call 일 때는 do 시리즈를 사용하는 것도 추천된다.


<doReturn when>

Properties prop = new Properties();
Properties spyProp = spy(prop);
doReturn("42”).when(spyProp).get("shoeSize");
String value = spyProp.get(“shoeSize”);
assertEquals(“42”, value);


<doThrow when>

CalcService calcService = new CalcService();
spyCalcService = spy(calcService);
doThrow(new RuntimeException(“divide operation not implemented”)).when(spyCalcService).divide(10.0, 20.0);



-

@Spy 또는 spy() 는 mock 이 아닌 real object 를 wrapping 한다.

그래서 추가로 when 문 등이 정의되지 않은 녀석들은 실제 object 의 함수들이 호출된다.

List<String> list = new LinkedList<>();
List<String> spy = spy(list);
// when(spy.get(0)).thenReturn(“foo”); 위에서 봤지만 이 경우는 stubbing 되는게 아니라 real method 가 호출된다.
doReturn(“foo”).when(spy).get(0);
assertEquals(“foo”, spy.get(0));



-

spy 를 통해 partial mock 을 만들수도 있지만, 1.8.0 부터는 mock 을 partial mock 처럼도 쓸 수 있다.

Foo mock = mock(Foo.class);
when(mock.someMethod()).thenCallRealMethod();



-

Mockito 에서는 verify 를 통해서 mock method 가 불렸는지 확인할 수 있다.

이를 behavior testing 이라고 하는지, 결과를 보는 것이 아니라 해당 동작을 했는지를 확인하는 것이다.

verify(calcService).add(10.0, 20.0);


아래와 같은 syntax 로 작동한다.

verify(mock).verifyFunction


즉 위의 코드는 calcService 에 add(10.0, 20.0) 이 불렸는지를 확인해준다.



-

verify 는 mock method 가 불린 횟수도 확인 가능하다.

vefify(calcService, times(1)).add(10.0, 20.0);

위 코드는 calcService.add(10.0, 20.0) 은 한번만 불렸어야 함을 verify 한다.


times(int times) 외에도 다른 함수들이 들어갈 수 있다.

never()

atLeast(int min)

atLeastOnce

atMost(int max)



-

verifyNoMoreInteractions(mockObj) 를 해주면, 이전에 verify 로 등록해준 녀석들 외 다른 녀석들은 불렸으면 안 된다는 것을 얘기한다.

verifyZeroInteractions(mock1, mock2, … ) 는 해당 mock 에는 아무것도 호출되지 않았어야 함을 명시한다.



-

Annotation 들을 사용해서 세팅 과정을 쉽게 할 수 있다.

아래와 같은 class 가 있고, 이 녀석을 검증하고 싶다면..

public class ArticleManager {
    private User user;
    private ArticleDatabase database;

    public ArticleManager(User user, ArticleDatabase database) {
        super();
        this.user = user;
        this.database = database;
    }

    public void initialize() {
        database.addListener(new ArticleListener());
    }
}

@RunWith(MockitoJUnitRunner.class) // annotation 들 처리를 위해 써줘야 함
public class ArticleManagerTest  {

       @Mock ArticleCalculator calculator; // mock 으로 생성
       @Mock ArticleDatabase database;
       @Mock User user;

       @Spy private UserProvider userProvider = new ConsumerUserProvider(); // spy 로 생성

       @InjectMocks private ArticleManager manager; // manager 에게 mock 중에 전달할 수 있는 것들을 전달

       @Test public void shouldDoSomething() {
           // calls addListener with an instance of ArticleListener
           manager.initialize();

           // validate that addListener was called
           verify(database).addListener(any(ArticleListener.class));
       }
}


@InjectMocks 는 constructor injection, setter injection, property injection 순서로 작동한다.

만약 위의 예제에서 manager 가 constructor 에서 User 만 받고 있고, setter 로 database 를 받는 구조로 설계되어 있었다면, User 만 inject 된다.



-

method call 의 order 도 검증할 수 있다.

InOrder inOrder = inOrder(calcService) // mock 이 들어간다.
inOrder.verify(calcService).add(20.0, 10.0);
inOrder.verify(calcService).subtract(20.0, 10.0);


-

ArgumentCaptor 를 사용하면 verification 단계에서 argument 로 사용된 녀석들을 capture 할 수 있다.

public class MockitoTests {
    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Captor
    private ArgumentCaptor<List<String>> captor;
    // annotation 대신 captor = ArgumentCaptor.forClass(List.class); 와 같이 쓸 수도 있다.

    @Test
    public final void shouldContainCertainListItem() {
        List<String> asList = Arrays.asList("someElement_test", "someElement");
        final List<String> mockedList = mock(List.class);
        mockedList.addAll(asList);

        verify(mockedList).addAll(captor.capture()); // captor 에 addAll 로 불린 녀석들이 capture 된다.
        final List<String> capturedArgument = captor.getValue();
        assertTrue(capturedArgument.contains("someElement")); 
    }
}



-

Answer interface 를 사용하면, thenReturn 으로 단조로운 값만 return 하던 것을 벗어나

thenAnswer 를 통해 복잡한 계산 결과를 return 할 수 있다.

when(calcService.add(20.0,10.0)).thenAnswer(new Answer<Double>(){
    @Override
    public Double answer(InvocationOnMock invocation) throws Throwable {
        Object[] args = invocation.getArguments();
        Object mock = invocation.getMock();
        // do something with args & mock
        return 30.0;
    }
}

doAnswer(returnsFirstArg()).when(list).add(anyString()); // returnsFirstArg() 는 이미 구현된 Answer 객체 return
when(list.add(anyString())).thenAnswer(returnsFirstArg());
when(list.add(anyString())).then(returnsFirstArg());



-

mock 을 reuse 하려면 reset(calcService);



-

Behavior driven developmentgiven, when, then format 으로 이루어진다.

// given
given(calcService.add(20.0, 10.0)).willReturn(30.0);

// when
double result = calcService.add(20.0, 10.0);

// then
Assert.assertEquals(result, 30.0, 0);



-

timeout option 도 있다.

verify(calcService, timeout(100)).add(20.0, 10.0);
verify(calcService, timeout(100).times(2)).subtract(20.0, 10.0); // 2번 subtract 가 불렸어야 하며, 100ms 안에 끝났어야 한다.
verify(calcService, new Timeout(100, yourOwnVerificationMode)).subtract(20.0, 10.0);


-

원래 final class 들은 Mocking 이 안 되었는데, v2 부터 가능해졌다.

단, 이는 기본적으로 되는 것이 아니라서 org.mockito.plugins.MockMaker 파일을 src/test/resources/mockito-extensions/ 혹은 src/mockito-extensions/ 폴더에 만들어야 한다.

그리고 파일 내용물은 "mock-maker-inline” (따옴표 없이) 이여야 한다.



-

MockitoRule 을 만들때 Strict stub 옵션을 주면 실패할 경우 exception 이 발생하면서 테스트를 조기 종료할 수 있다.

@Rule public MockitoRule rule = MokitoJUnit.rule().strictness(Strictness.STRICT_STUBS);



-

Mockito 는 static method 를 mock 하지 못한다.

이 때 사용할 수 있는 것이 Powermock 이다. PowerMock 은 PowerMockito 라는 class 를 제공한다.


Powermock 이 별로라면 static method 를 wrapping 하는 class 를 만드는 것도 추천된다.



-

1.9.0 부터 one-liner stub 이 가능하다.

아래와 같이 한줄로 mock 생성과 조건 걸기를 할 수 있다.

Car boringStubbedCar = when(mock(Car.class).shiftGear()).thenThrow(EngineNotStarted.class).getMock();



-

2.2.x 부터는 mock object 에 대한 추가 정보를 얻을 수 있다.

Mokito.mockingDetails(someObject).isMock();
Mokito.mockingDetails(someObject).isSpy();

MockingDetails details = mockingDetails(mock);
details.getMockCreationSettings().getTypeToMock();
details.getMockCreationSettings().getDefaultAnswer();
details.getInteractions();
details.getStubbings();
details.printInvocations();


반응형

댓글2