Happy Coding, Happy Life

Mockito - Partial Mock

| Comments

Mockito是一个方便的java测试Mock工具。

当我们做TDD或者单元测试的时候,为了消除当前被测对象和其依赖之间的关系,经常使用Mockito。

本篇文章只涉及如何使用Mockito的Partial Mock。关于Mockito的其他用法,请参照Mockito

1. 什么是Partial Mock

在单元测试的过程中,偶尔会出现这种情况。对于一个对象,某些方法需要被mock,而某些方法希望被直接调用,这时候,我们就需要借助Partial Mock。

2. 使用Partial Mock的场景

在如下两种场景中,Partial Mock就显得相当犀利:

a) 测试存在具体方法的抽象类

1
2
3
4
5
6
7
8
public abstract class AbstractHandler {

    public String greet() {
        return "Hello " + fetchName() + "!";
    }
 
    protected abstract String fetchName();
}

如上代码所示,抽象类AbstractHandler中定义了方法greet(),并且该方法存在一定逻辑。 此时,如果希望测试greet(),可以选择两种方式:

  • 构建其派生非抽象类,如ConcertHandler,再进行测试。
  • 使用Partial Mock,它可以省去构建派生类的步骤,直接对该抽象类进行测试,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
public class AbstractHandlerTest {
      
    @Test
    public void shouldCallRealMethodsAndFakeAbstractMethod() {
        AbstractHandler handler = mock(AbstractHandler.class);

        when(handler.greet()).thenCallRealMethod(); 
        when(handler.fetchName()).thenReturn("Geek");

        assertThat(handler.greet(), is("Hello Geek!"));
    }
}

如代码所示,对于抽象方法fechName(),直接mock其返回值

when(handler.fetchName()).thenReturn("Geek");

对于被测方法greet(),则使用thenCallRealMethod()使Mock对象调用真实的实现方法。

when(handler.greet()).thenCallRealMethod();

b)测试过分依赖于I/O操作的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class BooksController {

    protected Map<String, Date> fetchBooks(){
        //fetch the books from different publishing company.
        service1.retrieveBooks();
        service2.retrieveBooks();
        ......
        service10.retrieveBooks();

    }

    protected List<String> getPublishedBooks(){

        books = fetchBooks();
        //only pick up the books published.
    }
}

如上代码所示,类BooksController中定义了方法fetchBooks(),并且该方法严重依赖于I/O操作。因为fetchBooks()从不同的供应商那里获取最近的书籍信息。

对于这种测试,也有两种方案:

  • Mock所有的service返回结果,然后测试getPublishedBooks()。
1
2
3
doReturn(xxxx).when(service1).retrieveBooks()
doReturn(xxxx).when(service2).retrieveBooks()
doReturn(xxxx).when(service3).retrieveBooks()
  • 除此之外,也可以考虑在测试getPublishedBooks()的时候,对fetchBooks()方法进行mock。如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BooksControllerTest {

    @Test
    public void shouldReturnPublishedBooks(){
        BooksController controller = spy(new BooksController());

        Map<String, Date> books = new HashMap<String, Date>();
        books.put("XXX1", "2014-2-1");
        books.put("XXX2", "2014-3-1");

        doReturn(books).when(controller).fetchBooks();

        assertThat(controller.getPublishedBooks(), is(....));
    }

}

在如上的代码中,controller是BookController一个真实的实例,非mock对象。 原因是当前被测类为BooksController,因此不能将该类的实例定义成mock。
另外,controller的fetchBooks()方法虽然被partial mock了,但其他的方法还是该实例真正的方法。

因此,Spy的作用是将原本被实例化的对象,附上了Mock的功能。

注意,该场景完全是为了演示Partial mock。在实际情况中,如将books作为实例变量,并提供setBooks(List books)方法,根本不需要mock就能完成对getPublishedBooks的测试。另外,对于这种使用Spy的Partial Mock场景,大多数情况也是因为类的设计违背了SRP原则。

3. 两种Partial Mock方式

让我们再回顾一下上面的例子,并总结一下Partial Mock的两种用法:

  • 使用Mock构建对象,再使用CallRealMethod指定某方法为真实调用。

  • 实例化对象,定义Spy,再使用doReturn(xxx).when(xxx).method()

4. 区别

起初,偶比较倾向于使用CallRealMethod。因为该方法名足够表意,很容易就能理解其带来的用途。 不过,使用CallRealMethod却不是推荐的方式,为什么呢,请看如下代码:

@Test
public void testListPartialMockByCallRealMethod(){
    List<Integer> list = mock(ArrayList.class);
    when(list.add(100)).thenCallRealMethod();

    list.add(100);
    verify(list).add(100);
}

代码没问题吧,不过运行后,却得到如下错误:

java.lang.NullPointerException
    at java.util.ArrayList.add(ArrayList.java:352)
    at com.wl.tw.VersionInfoCheckTest.testListByCallRealMethod(VersionInfoCheckTest.java:59)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

如果查看ArrayList.java:352的代码,就会发现

350    public boolean add(E e) {
351        ensureCapacity(size + 1);  // Increments modCount!!
352        elementData[size++] = e;
353        return true;
354    }

在第352行,ArrayList实际上使用了另外一个对象elementData来存放新加入的元素。

在这个例子中,由于ArrayList是mock的,并不是实例化出来的,所以mock的ArrayList并没有初始化elementData对象。这就是为什么会出现NullPointerException的原因。

5. 总结

  • 当使用mockito mock对象时,它并不会mock该对象内部的其他依赖对象。
  • 当使用mockito partial mock时,注意spy和CallRealMethod的区别。

Comments

comments powered by Disqus