alighters

程序、写作、人生

Android单元测试-Mockito 浅析

| Comments

本文主要针对测试框架 Mockito 在实践中的经常用到的代码做一示例汇总,并对其实现思想做以简单的分析。

介绍

用来为提供函数返回结果的模拟(mock)及对函数调用过程的验证。

关键词 + mock : 针对真实的类或者对象,创建一个模拟(代理)的对象。 + stub : 针对一个类或者对象的方法,进行模拟调用及输出。

其中 mock 针对是类和队形,而 stub 针对的是行为。他们具体在此框架中的体现分别是: 1) mock 对应的是类 Mockito 中的 mockspy 方法;2)stub 对应是 Mockito 中的 whendoReturn 等系列方法。

PS: 这里注意与框架 Robolectric 的 Shadow 以区别。

引入

1
testCompile 'org.mockito:mockito-core:2.1.0-beta.119'

代码示例:地址

1. Mock 方法的使用

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testMock() {
  List mockedList = mock(List.class);

  //using mock object
  mockedList.add("one");
  mockedList.clear();

  //verification
  verify(mockedList).add("one");
  verify(mockedList).clear();
}

可直接通过接口来进行 mock。一旦创建了一个 mock 之后,他会记住所有它的操作,则我们就可以通过 verify 方法来检查相应方法是否调用。

2.打桩(Stub),即调用返回的结果模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LinkedList mockedList = mock(LinkedList.class);

//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

//following prints "first"
System.out.println(mockedList.get(0));

//following throws runtime exception
System.out.println(mockedList.get(1));

//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

这里指定关键字 when 返回一个 OngoingStubbing 接口,通过其提供的 thenReturnthenThrowthenCallRealMethod 及自定义 thenAnswer 来返回相应的结果。

3.参数匹配

1
2
3
4
5
6
7
8
9
LinkedList mockedList = mock(LinkedList.class);
//stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");

//following prints "element"
System.out.println(mockedList.get(999));

//you can also verify using an argument matcher
verify(mockedList).get(anyInt());

有时我们针对函数参数的模拟,不是一个特定的数值,而是一个范围。这时可以范围型的参数匹配,在 ArgumentMatchers 中,提供了一组不同类型的 any 操作。如:any(Class)anyObject()anyVararg()anyChar()anyInt()anyBoolean()anyCollectionOf(Class)等。

4.调用次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LinkedList mockedList = mock(LinkedList.class);

//using mock
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

//following two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

//exact number of invocations verification
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

//verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened");

//verification using atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("twice");
verify(mockedList, atMost(5)).add("three times");

通过 timesneveratLeastOnceatLeastatMost 这些方法,我们可以对一个方法的调用次数做判断。其中 times(1) 是默认的。

5.方法添加异常

1
2
3
4
5
LinkedList mockedList = mock(LinkedList.class);
doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:
mockedList.clear();

使用 doThrow 可以为一个方法的调用添加异常。这样可以验证我们的代码对异常的处理能力如何。

6.顺序验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);

//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");

//create an inOrder verifier for a single mock
InOrder inOrder1 = inOrder(singleMock);

//following will make sure that add is first called with "was added first, then with "was added second"
inOrder1.verify(singleMock).add("was added first");
inOrder1.verify(singleMock).add("was added second");

// B. Multiple mocks that must be used in a particular order
List firstMock = mock(List.class);
List secondMock = mock(List.class);

//using mocks
firstMock.add("was called first");
secondMock.add("was called second");

//create inOrder object passing any mocks that need to be verified in order
inOrder1 = inOrder(firstMock, secondMock);

//following will make sure that firstMock was called before secondMock
inOrder1.verify(firstMock).add("was called first");
inOrder1.verify(secondMock).add("was called second");

若是我们需要对调用的顺序做判断,就可以使用 InOrder 这个类,通过 Mockito 的方法 inOrder,来作为其参数,这样我们的方法就必须按顺序调用。试试将上述代码的 verify 顺序交换,看看会发生什么。

7.调用从未发生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List mockOne = mock(List.class);
List mockTwo = mock(List.class);
List mockThree = mock(List.class);

//using mocks - only mockOne is interacted
mockOne.add("one");

//ordinary verification
verify(mockOne).add("one");

//verify that method was never called on a mock
verify(mockOne, never()).add("two");

//verify that other mocks were not interacted
verifyZeroInteractions(mockTwo, mockThree);

通过 never 来指定一个方法从未发生调用,使用 verifyZeroInteractions 来确定对象的实例从未发生调用

8. 没有更多调用

1
2
3
4
5
6
7
8
9
10
List mockedList = mock(List.class);

//using mocks
mockedList.add("one");
mockedList.add("two");

verify(mockedList).add("one");

//following verification will fail
verifyNoMoreInteractions(mockedList);

代码中的 verifyNoMoreInteractions 会发生错误,原因就在于未对 add("two") 做验证,我们在 verify(mockedList).add("one"); 代码后添加 add(two)的方法验证,最后的测试通过。

1.这里的 verify add("one")add("two)顺序是无所谓的。 2.可以看出的是这个测试方法的不精确性,尽力避免使用。

9. @Mock 注解

1
2
3
4
5
6
7
 public class ArticleManagerTest {

       @Mock private ArticleCalculator calculator;
       @Mock private ArticleDatabase database;
       @Mock private UserProvider userProvider;

       private ArticleManager manager;

可以通过对属性添加 @Mock 注解来避免使用 mock 方法,不过不要忘了 initMocks 方法的调用:

1
MockitoAnnotations.initMocks(testClass);

10. 连续调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HashMap mock = mock(HashMap.class);
when(mock.get("some arg")).thenThrow(new RuntimeException()).thenReturn("foo");

//First call: throws runtime exception:
try {
   mock.get("some arg");
} catch (Exception e) {
   System.out.println(e.toString());
}

//Second call: prints "foo"
System.out.println(mock.get("some arg"));

//Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.get("some arg"));

通过对 mock 一直添加 then 的返回值,使得我们按顺序每次调用的返回结果都不同。另外,一个简单的写法, thenReturn 支持数组参数,来设定结果依次返回:

1
 when(mock.someMethod("some arg")) .thenReturn("one", "two", "three");

11.Answer 结果返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HashMap mock = mock(HashMap.class);

when(mock.get(anyString())).thenAnswer(new Answer<Object>() {

   @Override
   public Object answer(InvocationOnMock invocation) throws Throwable {
       Object[] args = invocation.getArguments();
       Object mock = invocation.getMock();
       return "called with arguments: " + args[0];
   }
});

//the following prints "called with arguments: foo"
System.out.println(mock.get("foo"));

当我们一个函数方法返回结果的不确定性,需要动态地根据参数指来改变。则上述的几个 then 方法不满足的情况下,我们可以通过 thenAnswer 方法返回一个 Answer 对象,来动态地返回结果。

12.doReturn | doThrow | doAnswer | doNothing | doCallRealMethod

1
2
3
4
5
List mockedList = mock(LinkedList.class);
doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:
mockedList.clear();

使用 do 系列的方法,我们可以针对 返回值 的方法进行测试。

13.检测真实的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List list = new LinkedList();
List sypList = spy(list);

//optionally, you can stub out some methods:
when(sypList.size()).thenReturn(100);

//using the spy calls *real* methods
sypList.add("one");
sypList.add("two");

//prints "one" - the first element of a list
System.out.println(sypList.get(0));

//size() method was stubbed - 100 is printed
System.out.println(sypList.size());

//optionally, you can verify
verify(sypList).add("one");
verify(sypList).add("two");

mock 方法是根据接口、类动态地生成一个对象,若是我们有一个真正的对象的时候,其就不适用了,这时,可以使用 spy 方法。但是其有使用限制的:

1
2
3
4
5
6
7
8
List list = new LinkedList();
List spy = spy(list);

//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
//when(spy.get(0)).thenReturn("foo");

//You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);

使用 when + thenReturn ,并不返回我们预期的结果,而是需要使用 doReturn + when 的格式。 其原因在于,Mockito 框架并不会对真实的对象进行 mock,只会真实的对象创建一个副本。

14.指定返回信息

1
2
3
Map mock = mock(HashMap.class, Mockito.RETURNS_SMART_NULLS);

System.out.println(mock.get("b"));

添加了 Mockito. RETURNS_SMART_NULLS 参数,当调用未指定返回行为的方法,输出的内容将不再是简单的 null 异常,而是下面更加人性化的信息:

1
2
SmartNull returned by this unstubbed method call on a mock:
hashMap.get("b");

15.参数匹配判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ListOfTwoElements implements ArgumentMatcher<List> {
   public boolean matches(List list) {
       return list.size() == 2;
   }
   public String toString() {
       //printed in verification errors
       return "[list of 2 elements]";
   }
}

List mock = mock(List.class);

when(mock.addAll(argThat(new ListOfTwoElements()))).thenReturn(true);

mock.addAll(Arrays.asList("one", "two"));

verify(mock).addAll(argThat(new ListOfTwoElements()));

实现 ArgumentMatcher 类,并通过 argThat 方法对参数进行判断。

16.对真实类的部分 mock

这里一般有两种写法: 1) 使用 spy

1
2
3
4
5
6
7
8
9
10
@Test
public void testPartialRealMock1() {
  //you can create partial mock with spy() method:
  LinkedList linkedList = new LinkedList();
  linkedList.addFirst(1);

  List list = spy(linkedList);

  assertThat(list.get(0), is(1));
}

通过 spy 调用对象的方法,将会调用其真正的方法。

2) 使用 mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Rule
public ExpectedException thrown= ExpectedException.none();

@Test
public void testPartialRealMock2() {
  //you can enable partial mock capabilities selectively on mocks:
  List mock = mock(LinkedList.class);
  //Be sure the real implementation is 'safe'.
  //If real implementation throws exceptions or depends on specific state of the object then you're in trouble.

  when(mock.get(anyInt())).thenCallRealMethod();

  thrown.expect(Exception.class);

  mock.get(0);
}

针对 mock 的使用时,主要代码在于方法 thenCallRealMethod(),但它有个很大的安全隐患,就是此方法抛出异常的问题。上述代码就可以看出,因为真实的 list 对象,并不含有任何元素,所以在通过真实方法返回时,就会有异常产生。

这里,建议使用方法一 spy,来对真实的对象进行测试。

17.重置 mock

1
2
3
4
5
List mock = mock(List.class);
when(mock.size()).thenReturn(10);
mock.add(1);
reset(mock);
assertThat(mock.size(), is(0));

使用 reset 方法,可以将 mock 重置为初始状态。

18.序列化 mock

1
 List serializableMock = mock(List.class, withSettings().serializable());

若是 spy 的使用则如下:

1
2
List<Object> list = new ArrayList<Object>();
List<Object> spy = mock(ArrayList.class, withSettings() .spiedInstance(list) .defaultAnswer(CALLS_REAL_METHODS) .serializable());

19.timeout 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
List mock = mock(List.class);

when(mock.get(0)).thenReturn(1);

System.out.println(mock.get(0));


verify(mock, timeout(100)).get(0);
//above is an alias to:
verify(mock, timeout(100).times(1)).get(0);

System.out.println(mock.get(0));

verify(mock, timeout(100).times(2)).get(0);

verify(mock, timeout(100).atLeast(2)).get(0);

verify(mock, new Timeout(100, new VerificationMode() {
   @Override
   public void verify(VerificationData data) {

   }

   @Override
   public VerificationMode description(String description) {
       return null;
   }
})).get(0);

指定了 timeout 的延时,同时我们也可以其他的验证操作,例如 timesatLeast 等,另外,我们也可以自定义自己的验证规则 VerficationMode

20.ignoreStub方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//mocking lists for the sake of the example (if you mock List in real you will burn in hell)
List mock1 = mock(List.class), mock2 = mock(List.class);

//stubbing mocks:
when(mock1.get(0)).thenReturn(10);
when(mock2.get(0)).thenReturn(20);

//using mocks by calling stubbed get(0) methods:
//System.out.println(mock1.get(0)); //prints 10
System.out.println(mock2.get(0)); //prints 20

mock1.get(0);
verify(mock1).get(0);

//using mocks by calling clear() methods:
mock1.clear();
mock2.clear();

//verification:
verify(mock1).clear();
verify(mock2).clear();



//verifyNoMoreInteractions() fails because get() methods were not accounted for.
try {
   verifyNoMoreInteractions(mock1, mock2);
} catch (NoInteractionsWanted e) {
   System.out.println(e);
}

//However, if we ignore stubbed methods then we can verifyNoMoreInteractions()
verifyNoMoreInteractions(ignoreStubs(mock1, mock2));

当第一次调用 verifyNoMoreInteractions 时,直接出现异常,是因为之前也调用了 mock2.get(0),但是并没有进行 verify。 而一旦我们对添加了 ignoreStubs方法,则会忽略之前的 Stub 的方法,不会再有 verify的限制。 比较特殊的是 inOrder的方法,它会自带 ignoreStubs的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List list = mock(List.class);
when(list.get(0)).thenReturn("foo");

list.add(0);
System.out.println(list.get(0)); //we don't want to verify this
list.clear();

//verify(list).add(0);
//verify(list).add(0);
//verify(list).clear();


// Same as: InOrder inOrder = inOrder(list);
InOrder inOrder = inOrder(ignoreStubs(list));

inOrder.verify(list).add(0);
// this will have an error..
//inOrder.verify(list).get(0);
inOrder.verify(list).clear();
inOrder.verifyNoMoreInteractions();

代码中特殊的一点是使用了 inOrder,它并不会上面 System.out.println(list.get(0)); 做处理。

21. 获取 mock 详情

1
2
3
List list = mock(List.class);
assertThat(Mockito.mockingDetails(list).isMock(), is(true));
assertThat(Mockito.mockingDetails(list).isSpy(), is(false));

22.自定义错误信息

1
2
3
List list = mock(List.class);
when(list.get(0)).thenReturn(1);
verify(list, description("should print the get(0) result")).get(0)

官方文档还提供一些关于 Java8 函数式的更多用法,这里因为环境问题就不列举了,更多内容可查阅官方文档。

原理简单剖析

通过上面的示例,我们可以发现两个很重要的方法:mockverify

1.mock 类生成

这里是使用运行时生成代码的库 byte-buddy,而对应在 mockito 框架中实现的代码是在 MockBytecodeGenerator 类中。其中主要的代码在方法 generateMockClass 中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public <T> Class<? extends T> generateMockClass(MockFeatures<T> features) {
  DynamicType.Builder<T> builder =
          byteBuddy.subclass(features.mockedType)
                   .name(nameFor(features.mockedType))
                   .ignoreAlso(isGroovyMethod())
                   .annotateType(features.mockedType.getAnnotations())
                   .implement(new ArrayList<Type>(features.interfaces))
                   .method(any())
                     .intercept(MethodDelegation.to(DispatcherDefaultingToRealMethod.class))
                     .transform(Transformer.ForMethod.withModifiers(SynchronizationState.PLAIN))
                     .attribute(MethodAttributeAppender.ForInstrumentedMethod.INCLUDING_RECEIVER)
                   .serialVersionUid(42L)
                   .defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE)
                   .implement(MockAccess.class)
                     .intercept(FieldAccessor.ofBeanProperty())
                   .method(isHashCode())
                     .intercept(to(MockMethodInterceptor.ForHashCode.class))
                   .method(isEquals())
                     .intercept(to(MockMethodInterceptor.ForEquals.class));
  if (features.crossClassLoaderSerializable) {
      builder = builder.implement(CrossClassLoaderSerializableMock.class)
                       .intercept(to(MockMethodInterceptor.ForWriteReplace.class));
  }
  return builder.make()
                .load(new MultipleParentClassLoader.Builder()
                        .append(features.mockedType)
                        .append(features.interfaces)
                        .append(Thread.currentThread().getContextClassLoader())
                        .append(MockAccess.class, DispatcherDefaultingToRealMethod.class)
                        .append(MockMethodInterceptor.class,
                                MockMethodInterceptor.ForHashCode.class,
                                MockMethodInterceptor.ForEquals.class).build(),
                        ClassLoadingStrategy.Default.INJECTION.with(features.mockedType.getProtectionDomain()))
                .getLoaded();
}

这里便是通过 byte-buddy 来生成我们的 mock 类, 其中代码行 .intercept(MethodDelegation.to(DispatcherDefaultingToRealMethod.class)) 则是用来生成代理方法的类 ,其中 DispatcherDefaultingToRealMethod 是类 MockMethodInterceptor 的静态内部类。在对其调用时,最后会调到 MockHandlerImpl 类的实现方法 handle ,这个才是我们执行 mock 类方法每次调用的重头戏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public Object handle(Invocation invocation) throws Throwable {
  if (invocationContainerImpl.hasAnswersForStubbing()) {
      // 对 doThrow() 或者 doAnswer() 返回 void 格式的执行调用     
      InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
              mockingProgress().getArgumentMatcherStorage(),
              invocation
      );
      invocationContainerImpl.setMethodForStubbing(invocationMatcher);
      return null;
  }
// 验证规则获取
  VerificationMode verificationMode = mockingProgress().pullVerificationMode();

  InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
          mockingProgress().getArgumentMatcherStorage(),
          invocation
  );
// mock 进度状态的验证
  mockingProgress().validateState();

  // 当 verificationMode 不是空的时候,则表明在执行 verify 方法
  if (verificationMode != null) {
      // 检查 verificationMode 是否对应正确的 mock
      if (((MockAwareVerificationMode) verificationMode).getMock() == invocation.getMock()) {
          VerificationDataImpl data = createVerificationData(invocationContainerImpl, invocationMatcher);
          verificationMode.verify(data);
          return null;
      } else {
          // 对应的不是相同的 mock , 重新添加 verification mode
          mockingProgress().verificationStarted(verificationMode);
      }
  }

  // 对调用执行打桩
  invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);
  OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainerImpl);
  mockingProgress().reportOngoingStubbing(ongoingStubbing);

  // 对这次调用,查找是否有存在的 answer 
  StubbedInvocationMatcher stubbedInvocation = invocationContainerImpl.findAnswerFor(invocation);

  if (stubbedInvocation != null) {
      stubbedInvocation.captureArgumentsFrom(invocation);
      return stubbedInvocation.answer(invocation);
  } else {
      Object ret = mockSettings.getDefaultAnswer().answer(invocation);
      new AnswersValidator().validateDefaultAnswerReturnedValue(invocation, ret);

      // 重新设置调用的方法
      invocationContainerImpl.resetInvocationForPotentialStubbing(invocationMatcher);
      return ret;
  }
}

代码中可以看出这个代理方法也做验证及调用方法的记录,用来方便后续 verify 方法的验证。 另外,针对真实对象模拟的方法 spy ,其调用的也是 mock 方法,不同的是指定了 spiedInstance 或者 answer 指定的是 CALLS_REAL_METHODS

2. verify 方法的实现

可知 verify 是对 mock 对象的验证,其调用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
public <T> T verify(T mock, VerificationMode mode) {
  if (mock == null) {
      throw nullPassedToVerify();
  }
  if (!isMock(mock)) {
      throw notAMockPassedToVerify(mock.getClass());
  }
  MockingProgress mockingProgress = mockingProgress();
  VerificationMode actualMode = mockingProgress.maybeVerifyLazily(mode);
  mockingProgress.verificationStarted(new MockAwareVerificationMode(mock, actualMode));
  return mock;
}

通过获取到 mockingProgress,调用其方法 verificationStarted,将新的规则 actualMode 保存下来,并最后返回 mock 对象。之后,若是针对 verify 的对象调用方法,则会调到上文提到 MockHandlerImplhandle 方法,会执行下面的语句:

1
2
3
4
5
if (((MockAwareVerificationMode) verificationMode).getMock() == invocation.getMock()) {
    VerificationDataImpl data = createVerificationData(invocationContainerImpl, invocationMatcher);
    verificationMode.verify(data);
    return null;
}

这里的 VerificationDataImpl 则有两个属性:

1
2
private final InvocationMatcher wanted;
private final InvocationContainer invocations;

其中 invocations 保存着我们对 mock 对象的调用记录,而 wanted 则是我们需要 verify 的方法。而具体的验证规则也就是我们之前保存的 VerificationMode

总结

至此,我们总结了 Mockito 框架的多种使用方法,及其简单的原理实现。若是有小伙伴不甚明了,欢迎加入 qq 群:289926871 来一起讨论。

参考资料

版权归作者所有,转载请注明原文链接:/blog/2016/09/08/unit-test-mockito/

给 Ta 个打赏吧...

Comments