[Java] [Unit Test] Mocking Framework - Mockito

转到Java之后我还没有系统地学习那些常用的mock框架,平时写代码都是模仿着别人的code东抄抄西抄抄,不会的再去stack overflow找找答案。最近发现很多新人都是这样写单元测试的,然后看看代码库,同一个包里用不同的框架的大有人在。我并不是说这样不好,但是这样很容易误导新人。我遇见过两次有人在写单元测试的时候,annotation用的是一个mock框架,在setup的时候又是用的另一个框架,然后纠结着怎么跑不通,各种错误呢!🤷‍♀️我自己摸爬滚打了一阵之后,发现写单元测试的时候最好的参考文档并不是别人的代码,而是官方的Tutorial。
于是我开了这个系列,打算讲讲大家常用的JMockit和Mockito等框架,也借此机会系统学习一遍。这里假设读者对单元测试的基本知识、AAA (Arrange, Act, Assert)的三段式以及什么是mock都了解,而且这里的mock框架与你所用的单元测试的框架Junit/TestNG没有关系。
对于每个框架,我会先介绍它的主要的annotation和功能;然后以自己经常写到的use cases举例说明用这个框架该怎么写。


Mockito

1. Mockito能mock什么?

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.

Mockito在StackOverflow上被评为“the best mock framework for Java”肯定是有一定的道理的。它最大的缺点可能是无法mock那些static和final class或者private方法,但是PowerMockito弥补了这些不足。而且在面向对象的设计中我们越来越少地使用那些静态的或面向过程的设计,所以绝大多数情况Mockito都能很方便地用起来。

2. Annotations

  • @Mock:为被修饰的类型创建一个虚拟的实现,并把这个虚拟实现的实例赋给被修饰的对象。
  • @InjectMocks:被修饰的对象会自动注入被@Mock和@Spy注解的成员。
  • @Spy:也是一种mock的方式,只是不会为被修饰的对象创建一个虚拟的对象,但是可以mock对象的一些行为。
  • @RunWith:用来绑定一个runner来初始化测试数据,例如,当使用@Mock时我们需要用@RunWith(MockitoJUnitRunner.class)修饰测试类来初始化那些mock的成员。当然也可以在setup里显示地用MockitoAnnotations.initMocks(this)来初始化那些mock的成员。此外,在Junit中还有另一种方式,那就是利用Junit的Rule(MockitoRule)来初始化那些mock,@Rule public MockitoRule rule = MockitoJUnit.rule()

3. Mockito的套路

Mockito像其他的mock框架一样,提供了一套Behavior Driven Development的单元测试书写三段式:Given, When, Then。

//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);

所以,用Mockito写单元测试大概是下面这个流程。
0)创建mock
创建mock的方式有两种,一种是利用@Mock标注,另一种则是直接用mock()静态方法calcService = mock(CalculatorService.class);
1)添加behavior
Mockito有好几种添加behavior的方式,乍一看有点儿眼花缭乱,其实主要是分为两类。

  • When/Then or Given/Will
when(object.method()).thenReturn(value);
given(object.method()).willThrow(exception);
  • doXX/When
    之所以引入doXX是为了解决stub返回值为void的方法,因为上面的when()和given()的参数是T,当传入返回值为void的方法时会出现编译错误。
doThrow(new RuntimeException()).when(mockedList).clear();

2)执行测试
3)验证behavior
当验证behavior的时候我们一般验证指定的方法有没有被invoke,被调用了多少次,还可以capture被invoke时的参数值,用于做进一步的assert。

verify(calcService, times(1)).add(10.0, 20.0);
verify(calcService, never()).multiply(10.0,20.0);

对于确定的次数,一般用times(x)就可以了,比如never()也可以写times(0)。但是对于一些不确定但是有上下限的则可以用atLeast(int min)atLeastOnce()atMost(int max)

4. Use Cases

还是话不多说地从DemoService说起吧,下面我汇集了平时常用的一些情况在一个API中,然后我们尝试着用Mockito来写一下它的测试,并以此为出发点介绍Mockito的一些常见用法。

public class DemoService {
    private DependencyY dependencyY;
    private DependencyYY dependencyYY;
    private DependencyZ dependencyZ;

    public DemoService(DependencyZ dependencyZ) {
        this.dependencyZ = dependencyZ;
    }

    public int run() {
        DependencyX dependencyX = new DependencyX("inputOfX");
        String x = dependencyX.getX();
        List<String> y = dependencyY.getY("default");
        List<String> list = new ArrayList<>();

        for (String s : y) {
            try {
                list.add(dependencyYY.doSomethingForY(s));
            } catch (DemoException ex) {
                // Handle the exception
            }
        }

        int index = doSomething(list, x);
        if (StaticDependency.isEnabled() && -1 == index) {
            dependencyZ.sendNotification("ErrorMessage");
        }

        StaticDependency.doAnything();
        return index;
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({DemoService.class, StaticDependency.class})
public class DemoServiceTest {
    @Mock
    private DependencyX dependencyX;
    @Mock
    private DependencyY dependencyY;
    @Mock
    private DependencyYY dependencyYY;
    @Mock
    private DependencyZ dependencyZ;
    @InjectMocks
    private DemoService demoService = new DemoService(dependencyZ);
    @Captor
    private ArgumentCaptor<String> notificationCaptor;

    @Before
    public void setUp() throws Throwable {
        MockitoAnnotations.initMocks(this);
        PowerMockito.whenNew(DependencyX.class).withAnyArguments().thenReturn(dependencyX);
        PowerMockito.mockStatic(StaticDependency.class);
        PowerMockito.when(StaticDependency.isEnabled()).thenReturn(true);
    }

    @Test
    public void testDemoService_ShouldGetCorrectIndex_WhenXExistsInY() throws Exception {
        when(dependencyX.getX()).thenReturn("str2");
        when(dependencyY.getY(anyString())).thenReturn(Arrays.asList("B", "A", "C"));
        when(dependencyYY.doSomethingForY("B")).thenReturn("str1");
        when(dependencyYY.doSomethingForY("A")).thenReturn("str2");
        when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());

        int result = demoService.run();

        Assert.assertEquals(1, result);
        PowerMockito.verifyStatic();
        StaticDependency.doAnything();
        verify(dependencyZ, times(0)).sendNotification(anyString());
    }

    @Test
    public void testDemoService_ShouldSendNotification_WhenXNotExistsInY() throws Exception {
        when(dependencyX.getX()).thenReturn("str3");
        when(dependencyY.getY(eq("default"))).thenReturn(Arrays.asList("B", "A", "C"));
        when(dependencyYY.doSomethingForY("B")).thenReturn("str1");
        when(dependencyYY.doSomethingForY("A")).thenReturn("str2");
        when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());

        int result = demoService.run();

        Assert.assertEquals(-1, result);
        PowerMockito.verifyStatic();
        StaticDependency.doAnything();
        verify(dependencyZ, atLeastOnce()).sendNotification(notificationCaptor.capture());
        Assert.assertTrue(notificationCaptor.getValue().contains("Error"));
    }
}
4.1 mock被测对象中的成员有返回值的方法

如果被测对象中的成员是一个外部依赖,我们需要mock它的那些有返回值的方法,例如dependencyY.getY(x)dependencyYY.doSomethingForY(y)
这是最基本最常见的一种情况,我们直接mock这些dependency类,然后用when/then语句(given/will也类似)给我们用到的那些方法添加behavior。

when(mockedClass.method(params)).thenReturn(mockedResult);
when(dependencyY.getY(anyString())).thenReturn(Arrays.asList("B", "A", "C"));
when(dependencyYY.doSomethingForY("B")).thenReturn("str1");

when(mockedClass.method(params)).thenThrow(exception);
when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());
  • 如果被mock的方法有参数值,你很确定在测试的时候这个值是什么的话,可以直接写这个值或者用eq。
  • 如果被mock的方法有参数值,但是不确定或者不在乎测试的时候这个值是什么的话,可以用any(), any(YourClass.class)或者是anyString()anyList()这种带具体类型的Matchers
  • 如果被mock的方法有参数值,不确定参数的具体值,但是有一个的规则,则可以用一些条件匹配。例如,isNull, isNotNull, notNull, contains(String substring), startsWith(String prefix), endsWith(String suffix), matches(String regex)。或者用argThat(Matcher<T> matcher)来写一些你自定义的匹配方法。
  • 当mock的方法的返回值需要用户自定义且比较复杂的时候,可以用thenAnswer来写一个answer方法来根据参数确定返回值,例如对dependencyYY.doSomethingForY()的mock可以用thenAnswer改写成下面的样子。
Map<String, String> mapYToSomething = new HashMap<String, String>() {{
    put("B", "str1");
    put("A", "str2");
    put("C", "str3");
}};

when(dependencyYY.doSomethingForY(anyString())).thenAnswer(new Answer<String>() {
    @Override
    public String answer(InvocationOnMock invocation) throws Throwable {
        Object[] args = invocation.getArguments();
        Object mock = invocation.getMock();
        return mapYToSomething.get(args[0]);
    }
});
4.2 mock被测对象中的成员没有返回值的方法

当被测对象依赖外部dependency的方式是一个没有返回值的方法时,我们在测试的时候并不希望这个方法被真正的调用,例如dependencyZ.sendNotification(message),但是我们依旧希望验证的是这个方法在确实被调用了。这个时候需要用verify语句对它们进行验证。

verify(mockedClass [, times(x), timeout(t)]).method(params);
verify(dependencyZ, times(0)).sendNotification(anyString());
verify(dependencyZ, never()).sendNotification(anyString());

verify(dependencyZ).sendNotification(anyString());
verify(dependencyZ, times(1)).sendNotification(anyString());

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

和上面when语句里方法的参数一样,这里的参数列表也可以写一个确定的值,或者任意值,或者匹配一定条件的值。

  • Capturing arguments
    当这里的参数值很复杂,我们想写一个合适的Matcher很复杂的时候,也可以用Capturing技术先把参数放进一个Captor中,然后再对其中的值进行验证,例如,上面例子里的notificationCaptor
@Captor
private ArgumentCaptor<String> notificationCaptor;

verify(dependencyZ).sendNotification(notificationCaptor.capture());
Assert.assertTrue(notificationCaptor.getValue().contains("Error"));

注意,虽然我把verify放在这个section讲,但是并不表示它只能用来验证返回值为空的方法,有返回值的方法同样适用。只是一般有返回值的方法我们会mock它的返回值,如果它确实拿到那个mocked值,那说明它执行了,就没必要再做多余的验证了。

  • Verify methods in order
    另外,如果我们对被mock对象的方法的执行顺序有要求的时候,可以先定义一个所mock对象的InOrder,然后用InOrder.verify()依你期望的顺序去验证它的一些方法。
InOrder inOrder = inOrder(calcService);
inOrder.verify(calcService).add(20.0,10.0);
inOrder.verify(calcService).subtract(20.0,10.0);
4.3 Partial Mocking

虽然Mockito支持用Spy去partial mock一些真实的instance,但是使用它的时候还是需要谨慎。对于spy的mock,最好使用doReturn|Answer|Throw()这类方法。

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);

还需要注意的是,Mockito的spy只是copy了你所mock的那个真实的instance,你之后的交互应该跟这个copy的spying instance交互,它不能检测到任何跟原来的real instance的交互。例如,上面的List有两个instance,一个是真实创建出来的LinkedList: list,另一个就是带有mock的spy(list)。之后如果我们对list进行操作,spy是不知情的。
此外,对于spy的object不要去试图mock final方法。

Watch out for final methods. Mockito doesn't mock final methods so the bottom line is: when you spy on real objects + you try to stub a final method = trouble. Also you won't be able to verify those method as well.

4.4 mock在被测对象中的直接new出来的对象

当我们需要mock那些直接在被测方法中new出来的对象的时候,直接在单元测试中用Mock还做不到JMockit的Mocked那么强大,比如说上面例子中的dependencyX。我们的做法是用PowerMockito去mock这个类的构造函数,返回一个我们mock的instance。关于PowerMockito的更多信息,我们将在下一节中介绍。
需要注意的是,当使用PowerMockito.whenNew()的的时候需要在PrepareForTest里加上需要new ClassA的被测试的ClassB。例如上面的例子,我们在DemoService类里直接调用new DependencyX()来创建一个DependencyX的实例,我们需要prepare的不是DependencyX而是DemoService。

@Mock
private DependencyX dependencyX;

PowerMockito.whenNew(DependencyX.class).withAnyArguments()
            .thenReturn(dependencyX);
when(dependencyX.getX()).thenReturn("str2");

另外,当需要验证被测试的逻辑中有构造某个对象,则可以用PowerMockito.verifyNew

PowerMockito.verifyNew(MyClass.class).withNoArguments();
4.5 mock static方法

对于静态方法的mock,我们就需要用到PowerMock了。
(0)使用PowerMockito的时候需要在单元测试类上添加annotation @RunWith(PowerMockRunner.class)@PrepareForTest({YourStaticClass.class})
为什么我们需要PrepareForTest呢,这里是官方文档里的介绍。总之,就是告诉PowerMock那些测试中需要mock的类,尤其是那些final类和需要mock私有的,静态的或者native的方法的类。

This annotation tells PowerMock to prepare certain classes for testing. Classes needed to be defined using this annotation are typically those that needs to be byte-code manipulated. This includes final classes, classes with final, private, static or native methods that should be mocked and also classes that should be return a mock object upon instantiation.

当使用这个PrepareForTest的时候,我们还要么加上@RunWith(PowerMockRunner.class),要么用下面的语句来初始化prepare的那些需要mock的东西。

public static TestSuite suite() throws Exception {
    return new PowerMockSuite(MyTestCase.class);
}

(1)Mock一个static类和它的方法,例如上面的StaticDependency. doAnything()

PowerMockito.mockStatic(StaticClass.class);
Mockito.when(StaticClass.staticMethod(param)).thenReturn(value);

(2)验证mock对象的行为。与上面一样,也需要先声明一下verifyStatic,然后直接验证具体的方法,连verify()都不需要了。如果方法有参数列表,则对参数的匹配可以用Mockito.Matchers那一套。注意,这两步是验证一个static方法必不可少的。当需要验证多个static的方法时,每个方法都需要加上verifyStatic()

PowerMockito.verifyStatic(StaticClass.class); 
StaticClass.staticMethod(param);

(3)mock static的方法抛出异常。对于有返回值的方法,也可以直接用when/thenThrow,对于没有返回值的方法,则需要用doThrow/when(staticClass),然后再写相应的方法。

PowerMockito.doThrow(new RuntimeException()).when(StaticDependency.isEnabled());
PowerMockito.doThrow(new RuntimeException()).when(StaticDependency.class);
StaticDependency.doAnything();

PowerMockito.doThrow(new RuntimeException()).when(myFinalMock).myFinalMethod();

(4)mock和verify私有的方法。对于私有的方法,我们直接用mockedClass.是无法访问到的,可以用下面这种方式。

PowerMockito.when(tested, "privateMethodName", argument).thenReturn(value);

PowerMockito.verifyPrivate(tested).invoke("privateMethodName", argument);

(5)跟Mockito类似,我们可以用PowerMockito.spy来实现Partial Mocking,下面是从powermock wiki拷贝的一个完整的例子。

@RunWith(PowerMockRunner.class)
// We prepare PartialMockClass for test because it's final or we need to mock private or static methods
@PrepareForTest(PartialMockClass.class)
public class YourTestCase {
    @Test
    public void spyingWithPowerMock() {        
        PartialMockClass classUnderTest = PowerMockito.spy(new PartialMockClass());

        // use Mockito to set up your expectation
        Mockito.when(classUnderTest.methodToMock()).thenReturn(value);

        // execute your test
        classUnderTest.execute();

        // Use Mockito.verify() to verify result
        Mockito.verify(mockObj, times(2)).methodToMock();
    }
}

References

Mockito Mockito-Core 2.7.12
Tutorialspoint Mockito Overview
Why is Mockito voted better than JMockit
Mockito vs. EasyMock vs. JMockit
Slant: JMockit vs. Mockito
Mockito vs. JMockit
Forming Mockito “grammars”
Parameterized testing with Mockito by using JUnit @Rule
Junit MockitoRule
PowerMock for Mockito
PowerMock Annotation PrepareForTest

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容