JMockit 中捕获 mock 对象方法的调用参数

三个月前写过一篇 Mockito 中捕获 mock 对象方法的调用参数,一般项目中 Mockito 不决求助于 JMockit, 同样的在 JMockit 也需对捕获被 Mock 的方法调用参数。当我们用 new Expectations(){{}} 打桩并在后面断言了返回值,那就无需捕获参数来断言,匹配到了方法调用即证明传入的参数也是对的,如下面的代码所示

 1public class UserServiceTest {
 2
 3    @Mocked
 4    private UserDao userDao;
 5
 6    @Test
 7    public void couldCallUserDaoToAddUser() {
 8        new Expectations(){{
 9           userDao.findById(123);
10           result = "Yanbin";
11        }};
12
13        UserService userService = new UserService(userDao);
14        String user = userService.findBy(123);
15
16        assertThat(user).isEqualTo("Yanbin");  //这里断言成功也就证明了 userDao.findById(123) 方法被调用,参数必须是 123
17    }
18}

但如果是未打桩的方法,或打桩是用的模糊参数(withInstanceOf(String.class)), 或是无返回值的方法就要事后对是否调用了某个方法以及传入什么参数的情况进行断言。
 1public class UserServiceTest {
 2
 3    @Mocked
 4    private UserDao userDao;
 5
 6    @Test
 7    public void couldCallUserDaoToAddUser() {
 8        UserService userService = new UserService(userDao);
 9        userService.findBy(123);
10
11        new Verifications(){{
12           userDao.findById(123); times = 1;
13        }};
14    }
15}

再或者是中间调用曲折,调用参数无法直接提供的话,这时候就需要对调用参数进行捕获再断言。下面是一个完整的例子,立即来检验一下 JMockit 是如何操作的

首先是待测试的 UserService 类
 1package cc.unmi;
 2
 3public class UserService {
 4
 5    private final UserDao userDao;
 6
 7    public UserService(UserDao userDao) {
 8        this.userDao = userDao;
 9    }
10
11    public void add(int id, String name) {
12        int calculatedId = id * 333 - 222;  //这里只用来说明调用外部接口时的参数 calculatedId 不总是很明了
13        userDao.add(calculatedId, name);
14    }
15}

和我们要 Mock 的 UserDao  接口,Mock 的优越性这里表现在我们根本没有接口实现,但并不妨碍我们写出完整的 UserService 的单元测试
1package cc.unmi;
2
3interface UserDao {
4    void add(int id, String name);
5}

接着就是我们捕获参数的测试代码
 1package cc.unmi;
 2
 3import mockit.Mocked;
 4import mockit.Verifications;
 5import org.junit.Test;
 6
 7import static org.fest.assertions.Assertions.assertThat;
 8
 9public class UserServiceTest {
10
11    @Mocked
12    private UserDao userDao;
13
14    @Test
15    public void couldCallUserDaoToAddUser() {
16        UserService userService = new UserService(userDao);
17        userService.add(123, "Yanbin");
18
19        new Verifications() {{
20            int userId;
21            String username;
22            userDao.add(userId = withCapture(), username = withCapture());
23
24            assertThat(userId).isEqualTo(40737);
25            assertThat(username).isEqualTo("Yanbin");
26        }};
27    }
28}

关键就是在上面的
userDao.add(userId = withCapture(), username = withCapture());
JMockit 恰到好处的用到 Java 的赋值语句返回等号右边的结果,相较而言 Mockito 捕获参数的方式就稍显笨拙,以下是 Mockito 的方式
ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(User.class); verify(userDao, times(1)).save(argumentCaptor.capture());
具体可参考前一篇文章 Mockito 中捕获 mock 对象方法的调用参数, 尤其是对于带泛型的参数的捕获 Mockito 一定要在声明带 @Captor 注解实例级的变量。而 JMockit 不管多复杂类型参数的捕获都毫无区别。

注意到 JMockit 在捕获参数是用的 withCapture(...) 方法,它有三个变体

上面刚刚用过的 withCapture() 简单了,就是直接把实际方法接受的值赋给一个变量。

withCapture(List<T> valueHolderForMultipleInvocations) 是在被 Mock 方法多次调用时,它会捕获每次调用的参数放到一个 List 中去。譬如在 UserService 中有一个方法用于批量删除多个用户
1//在 UserService 中
2public void delete(Integer... ids) {
3    for(int id: ids) {
4        userDao.delete(id);
5    }
6}

现在要捕获多次调用的参数就得
 1@Test
 2public void couldDeleteUsersInBatch() {
 3    new UserService(userDao).delete(1, 2, 3);
 4
 5    new Verifications() {{
 6        List<Integer> userIds = new ArrayList<>();
 7        userDao.delete(withCapture(userIds)); times = 2;
 8
 9        assertThat(userIds).hasSize(3);  //继续断言具体的元素值
10    }};
11}

和 Mockito 的捕获后调用 argumentCaptor.getValues() 来取得多次调用的参数类似。

最后一个方法 withCapture(T constructorVerification) 是 Mockito 所不能的,因为 Mockito 是不能 Mock 构造函数的,而 JMockit 是无所不能的。官方文档对此的解释是

捕获在单元测试执行中新创建的实例,这些实例必须是由对构造函数调用时参数匹配时创建的。

还是有点矇,主要还是没怎么感觉到这个方法有什么实际意义,还未到某时非用不可以状况,作以直接贴一下官方的示例代码
 1@Test
 2public void capturingNewInstances(@Mocked Person mockedPerson) {//Person 的所有实例都将被 Mock, Maven 的测试方法似乎不允许带参数
 3   // From the code under test:
 4   dao.create(new Person("Paul", 10));
 5   dao.create(new Person("Mary", 15));
 6   dao.create(new Person("Joe", 20));
 7
 8   new Verifications() {{
 9      // Captures the new instances created with a specific constructor.
10      List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));
11
12      // Now captures the instances of the same type passed to a method.
13      List<Person> personsCreated = new ArrayList<>();
14      dao.create(withCapture(personsCreated));
15
16      // Finally, verifies both lists are the same.
17      assertEquals(personsInstantiated, personsCreated);
18   }};
19}

主要还是掌握下前面两个捕获参数的方法

  1. <T> T withCapture()   捕获单调用的参数值,以返回值的形式给下捕获到的值,所以可用 userDao.add(userId = withCapture(), ...) 的形式
      如果参数本身是列表也是用这种形式:
    List<Person> people;
    dao.save(people = withCapture());
    assertEquals(2, people.size()); 
  2. <T> T withCapture(List<T> valueHolderForMultipleInvocations)  捕获多次调用的参数放到一个列表中,它的返回值只会保留最后一次的调用参数,需要获得全部捕获的值必须传递一个 List<T> 给 withCapture 方法。这类似于 Mockito 在捕获多次调用参数后,captor.getValues() 获得所有,而 captor.getValue() 只是最后一次的调用参数值。

参考:Capturing invocation arguments for verification 永久链接 https://yanbin.blog/jmockit-capture-mock-call-arguments/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。