使用 JMockit 来 mock 构造函数

Java 测试的 Mock 框架以前是用 JMockit, 最近用了一段时间的 Mockito, 除了它流畅的书写方式,经常这也 Mock 不了,那也 Mock 不了,需要迁就于测试来调整实现代码,使得实现极不优雅。比如 Mockito 在 私有方法,final 方法,静态方法,final 类,构造方法面前统统的缴械了。powermock 虽然可作 Mockito 的伴侣来突破 Mockito 本身的一些局限,但是我一用它来 Mock 一个构造方法就出错


Caused by: java.lang.ClassNotFoundException: org.mockito.exceptions.Reporter

原因是 Mockito 变化太快,powermock 跟不上它的步伐 -- https://github.com/powermock/powermock/issues/684,于是我只能止步。

不得已再祭出 JMockit 这号称(也确实是)一无所不能的大杀器,在此见识一下它怎么 Mock 构造函数的

本篇实例所使用的 JMockit 版本是 1.30, 当前最新版 1.31, 由于尚未被 Maven 中央仓库收录,所以暂用 1.30。在 pom.xml 中如下方式引入
1<dependency>
2    <groupId>org.jmockit</groupId>
3    <artifactId>jmockit</artifactId>
4    <version>1.30</version>
5    <scope>test</scope>
6</dependency>

待测试的代码是类 Example, 代码如下

 1package cc.unmi;
 2
 3public class Example {
 4
 5    public String findOneUser(String category) {
 6        if("general".equals(category) || category.equals("admin")) {
 7            return new UserService(new UserDao(), category).findById(123);
 8        }
 9        throw new RuntimeException("Invalid category");
10    }
11}

它的 findOneUser(category) 方法中需要根据条件来创建一个 UserService 实例,所以未把 UserService 实例声明为 Example 的属性,通过 Example 的构造函数来传入,若如此就很容易用 Mockito 的 Mock 这个 UserService 实例了。
private UserService userService;

public Exmple(UserService userService) {
    this.userService = userServie;
}
上面的实现是 Mockito 最喜爱的口味了。但由于 userService 并不跟随 Example 创建,所以 Mockito 去 Mock findOneUser(category) 里的 new UserService(userDao,  "admin") 就显示得捉襟见肘了。

在使用 JMockit mock UserService 构造函数之前,贴出一下 UserService 的演示实现
 1package cc.unmi;
 2
 3public class UserService {
 4    private UserDao userDao;
 5    private final String category;
 6
 7    public UserService(UserDao userDao, String category) {
 8        this.userDao = userDao;
 9        this.category = category;
10    }
11
12    public String findById(int id) {
13        return userDao.findById(id);
14    }
15}

我们要测试的目标方法是 Example.findOneUser(category), 其中一个测试是 userService 实例的 findById(id) 方法获得什么它也返回什么,所以单元测试中的 service.findById(id) 方法不应该调用实际的 userDao 的相应方法,也就是我们要 Mock 的目的所在。所以本例中的 UserDao 的方法并未实现,如下
1package cc.unmi;
2
3public class UserDao {
4    public String findById(int id) {
5       throw new RuntimeException("not implemented");
6    }
7}

那么来看测试代码 ExampleTest
 1package cc.unmi;
 2
 3import mockit.Expectations;
 4import mockit.Mocked;
 5import org.junit.Test;
 6
 7import static org.junit.Assert.assertEquals;
 8
 9public class ExampleTest {
10
11    @Mocked
12    private UserService userService;
13
14    @Test
15    public void testFindOneUser() {
16        new Expectations() {{
17           new UserService(withInstanceOf(UserDao.class), "admin");
18           result = userService;
19
20           userService.findById(123);
21           result = "Hello Yanbin's blog";
22        }};
23
24        Example example = new Example();
25        String user = example.findOneUser("admin");
26        assertEquals("Hello Yanbin's blog", user);
27    }
28}

上面的测试 testFindOneUser() 顺利通过,这里的精髓就在
new UserService(withInstance(UserDao.class), "admin");
result = userService;
JMockit 只是把构造函数当成一个普通的有返回值的方法而已。

我们也可以换一种方式来 Mock 构造函数,用  new MockUp<Class> 的方式,用以植入 Mock 的内部变量值,下面的例子不直接 Mock UserService 实例,而是通 $init 函数来改变生成的 userService 实例的内部状态,以便对它内部的操作作进一步精细的控制。$init 在 JVM 中就就构造函数的表示法。

下面的例子,Mockito 和 JMockit 双管齐下,结合 JMockit 的强悍功能,以及 Mockito 的 BBD 风格,你也可以只使用 JMockit 的 Expectations API 来 mock 对 mockedUserDao 的操作。
 1package cc.unmi;
 2
 3import mockit.Deencapsulation;
 4import mockit.Invocation;
 5import mockit.MockUp;
 6import org.junit.Test;
 7import org.junit.runner.RunWith;
 8import org.mockito.runners.MockitoJUnitRunner;
 9
10import static org.junit.Assert.assertEquals;
11import static org.mockito.Mockito.when;
12
13@RunWith(MockitoJUnitRunner.class)
14public class ExampleTest {
15
16    @org.mockito.Mock
17    private UserDao mockedUserDao;
18
19    @Test
20    public void testFindOneUser()  {
21
22        new MockUp<UserService>() {
23            @mockit.Mock
24            public void $init(Invocation invocation, UserDao userDao, String category) {
25                UserService userService = invocation.getInvokedInstance();
26                Deencapsulation.setField(userService, mockedUserDao);
27            }
28        };
29
30        when(mockedUserDao.findById(123)).thenReturn("Hello Again");
31
32        Example example = new Example();
33        String user = example.findOneUser("admin");
34        assertEquals("Hello Again", user);
35    }
36}

MockUp 和 Expectations API 其实也是 JMockit 的两种书写方式,前者可用于 mock 私有方法,后者常用,但功能稍弱一些。 永久链接 https://yanbin.blog/jmockit-mock-constructors/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。