JAVA单元测试mock框架

一.概述

最近在做代码重构,发现系统中的UT很少,重构没有UT的话,全部得人工测试,逻辑覆盖不一定全部能覆盖到,因此UT还是很有必要存在的。在写UT的时候,mock是必须要有的,但是现在适用于java代码做单元测试的mock框架很多,我们该如何选择?

在做选择之前,我们先看看如何使用每个mock框架,再做决定。

二.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
public class MockClazz {

    public String run(String name) {
        System.out.println(name);
        return name + " begin run...";
    }

    public static String sleep (String name) {
        System.out.println(name);
        return name + " begin sleep...";
    }

    private String eat(String name) {
        System.out.println(name);
        return name + " begin eat...";
    }

    public String getEatInfo(String name) {
        return eat(name);
    }

    public final String create(String name) {
        return name;
    }
}

1.easymock

easymock是比较早的一个mock框架,做一次mock需要先创建一个mock对象,然后录制mock代码,把mock对象切换到播放状态,执行单元测试,最后再验证mock对象是否按照录制的mock行为执行。
引入easymock的依赖

1
2
3
4
5
<dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>3.4</version>
</dependency>

如何编写mock代码,分为五个步骤

1
2
3
4
5
6
7
8
9
10
11
// ① 创建mock对象
MockClazz mockClazz = EasyMock.createMock(MockClazz.class);
// ② 录制mock对象的预期行为和输出
EasyMock.expect(mockClazz.run(EasyMock.anyString())).andReturn("mocked string for run");
// ③ 将mock对象切换到播放状态
EasyMock.replay(mockClazz);
// ④ 调用mock对象方法进行测试
String actualString = mockClazz.run("name");
Assert.assertEquals("mocked string for run", actualString);
// ⑤ 对mock对象的行为进行验证,验证mock的对象是否按照录制的行为发生
EasyMock.verify(mockClazz);

这样我们就使用easymock完成了一个对象的mock测试。
在上面的例子中,我们只mock了run这个方法,那没有被mock的方法,在调用的时候会出现什么问题?

1
String eatInfo = mockClazz.getEatInfo("aa");

上述代码执行的时候出现下面的异常

java.lang.AssertionError:
Unexpected method call MockClazz.getEatInfo(“aa”):
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:44)
…..

可见easymock如果其中一个方法没有被mock但是被调用了,就会抛异常。easymock不支持private,final,static等方法的mock

2.mockito

mockito是在easymock之后出现的,相对于easymock来说,mockito少了对象状态切换这一步骤。
引入mockito的依赖

1
2
3
4
5
<dependency>
     <groupId>org.mockito</groupId>
     <artifactId>mockito-all</artifactId>
     <version>2.0.2-beta</version>
</dependency>

如何编写mockito的mock代码,分四个步骤

1
2
3
4
5
6
7
8
9
// ① 创建mock对象
MockClazz mockClazz = Mockito.mock(MockClazz.class);
// ② 录制mock代码
Mockito.when(mockClazz.run("A")).thenReturn("B");
// ③ 执行单元测试
String actual = mockClazz.run("A");
Assert.assertEquals(actual,"B");
// ④ 校验mock对象的行为是否按照mock执行
Mockito.verify(mockClazz).run("A");

和easymock相比,mockito少了一个环节,就是把对象切换到播放状态。
上面的代码中我们mock了run方法,但是getEatInfo方法没有被mock,调用这个方法会出现什么问题

1
2
String eatInfo = mockClazz.getEatInfo("aa");
System.out.println(eatInfo);

此时返回null,按照mockito的官方文档,没有被mock的方法返回默认值,具体可以看mockito的官方文档。
那么mockito如何保证不被mock的代码按照原来的逻辑输出呢?

通过doCallRealMethodl来实现

1
2
3
4
5
6
MockClazz mockClazz = Mockito.mock(MockClazz.class);
Mockito.doCallRealMethod().when(mockClazz).run("A");
String actual = mockClazz.run("A");
System.out.println(actual); // A begin run... 原样执行

System.out.println(mockClazz.run("B")); // null 返回默认值

上面的代码显示指定了通过run(“A”)的时候调用原来的代码执行,输出A返回A begin run…

通过spy来实现

1
2
3
4
5
MockClazz mockClazz = Mockito.spy(new MockClazz()); // 注意这里需要new一个
Mockito.when(mockClazz.run("A")).thenReturn("B");

String actual = mockClazz.run("C");
System.out.println(actual); // 输出[C begin run...],原样输出忽略mock逻辑

此时mockClazz.run(“C”)直接按照原来的代码执行,忽略mock逻辑。
在使用spy的时候需要注意一个点,看下面两段代码

1
2
3
MockClazz mockClazz = Mockito.spy(new MockClazz());
Mockito.when(mockClazz.run("A")).thenReturn("B");
System.out.println(mockClazz.run("A")); // 实际执行run的代码,只是修改返回值(先输出A,再返回B begin run...)

这段代码只是修改了返回值,实际代码逻辑被执行了,也就是说这种mock逻辑只是mock了返回值,类似SpringAOP在方法返回的时候拦截一下修改了返回值。

1
2
3
MockClazz mockClazz = Mockito.spy(new MockClazz());
Mockito.doReturn("B").when(mockClazz).run("C");
System.out.println(mockClazz.run("C")); // 根本不执行run的代码,直接返回

这段代码不仅该了返回值,同时也真正的代码一行也不会执行。

注意这两种写法的微妙区别哦
Mockito.doReturn(“B”).when(mockClazz).run(“C”);
Mockito.when(mockClazz.run(“A”)).thenReturn(“B”);

mockito不支持private,final,static等方法的mock。

3.powermock

powermock实在easymock和mockito的基础上扩展而来的,easymock和mockito不能解决private,final,static等方法的mock,powermock为此提供了解决方案。powermock需要和easymock或者mockito配合起来一起使用。
引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <version>1.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>1.5</version>
    <scope>test</scope>
</dependency>
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
@RunWith(PowerMockRunner.class)
@PrepareForTest( { MockClazz.class })
public class MockTest {

    @Test
    public void testMockStatic() {
        // 静态方法的mock
        PowerMockito.mockStatic(MockClazz.class);
        PowerMockito.when(MockClazz.sleep("A")).thenReturn("B");
        System.out.println(MockClazz.sleep("A"));
        PowerMockito.verifyStatic();
    }

    @Test
    public void testMockPrivate() throws  Exception {
        // 私有方法的mock,getEatInfo=>eat,eat是私有方法
        MockClazz mockClazz = PowerMockito.mock(MockClazz.class);
        PowerMockito.when(mockClazz, "eat", "A").thenReturn("mock");
        PowerMockito.doCallRealMethod().when(mockClazz).getEatInfo("A");
        System.out.println(mockClazz.getEatInfo("A"));
    }

    @Test
    public void testMockFinal() {
        // final方法的mock
        MockClazz mockClazz = PowerMockito.mock(MockClazz.class);
        PowerMockito.when(mockClazz.create("A")).thenReturn("B");
        System.out.println(mockClazz.create("A"));
    }
}
4.jmockit

jmockit是一个轻量级的mock框架,内部采用ASM来修改字节码。
引入依赖

1
2
3
4
5
<dependency>
    <groupId>com.googlecode.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.0</version>
</dependency>

具体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
54
55
56
57
58
59
60
61
62
63
64
@RunWith(JMockit.class)
public class MockTest {

    @Mocked
    MockClazz mockClazz = new MockClazz();

    @Test
    public void testExpectations() { // 全局mock抛异常
        new Expectations() {
            {
                mockClazz.getEatInfo("A");
                returns("B");
            }
        };

        // 被mock的方法,返回mock后的值
        System.out.println(mockClazz.getEatInfo("A"));
        // 没有被mock的方法,mockit.internal.UnexpectedInvocation,jmocit对run没有进行mock
        System.out.println(mockClazz.run("A"));
    }

    @Test
    public void testNonExpectations() { //全局mock返回缺省值

        new NonStrictExpectations() {
            {
                mockClazz.getEatInfo("A");
                returns("B");
            }
        };

        // 被mock的方法,返回mock后的值
        System.out.println(mockClazz.getEatInfo("A"));
        // 没有被mock的方法,返回默认值,jmockit对run方法也进行了mock
        System.out.println(mockClazz.run("A"));
    }

    @Test
    public void testMockStatic() {
        // mock静态方法
        new NonStrictExpectations() {
            {
                MockClazz.sleep("A");
                result = "B";
            }
        };
        System.out.println(MockClazz.sleep("A"));
    }

    @Test
    public void testMockPrivate() {
        // mock静态方法
        final MockClazz obj = new MockClazz();
        new NonStrictExpectations(obj) {
            {
                // 私有方法mock
                this.invoke(obj, "eat", "A");
                returns("B");
            }
        };
        System.out.println(obj.getEatInfo("A")); // 私有方法被mock了
        System.out.println(obj.run("A")); // run方法不会被mock,走真实逻辑
    }
}

注意:
1.NonStrictExpectations返回缺省值针对没有mock的方法
2.Expectations针对没有mock的方法直接抛异常
3.官网上的jmockito暂时不支持私有方法的mock,google提供的高版本二方库也不支持私有方法的mock

上面例子的代码https://github.com/yangbolin/java-mock-framework

三.总结

本文依次对比了easymock,mockito,powermock,jmockito四个java的mock框架,easymock,mockito都存在final,private,static方法没法mock的问题,powermock解决了这个问题,jmockit也没有这个问题,powermock和jmockit的区别就在于API的风格,powermock继承了easymock和mockito的风格,链式的API风格,非常清晰,jmockit有自己的API风格。