Mockito 中捕获 mock 对象方法的调用参数
Mockito 可以帮助我们创建 Mock 对象,mock 被调用的方法,断言调用次数,在方法参数不易确定的情况下还能帮我们捕获参数。下面是我们第一个问题:
如果 UserService 的 save(user) 最终操作的不是同一个对象,它的实现稍加变化如下
由于在调用
但用 any() 的话我们会有些缺乏安全感,太含糊,可能实际传递给
当然如果我们清楚知道
如果你所用的 mockito 版本不够高的话,`ArgumentMatcher` 可能还不是个 FunctionalInterface, 就得老老实实的用匿名类,
注: 在写在此文时用的 mockito-core-2.6.2. 有些项目中或许仍在用 mockito-all 引入 mockito 依赖,这种方式无法引入 Mockito 2.*. 见 mockito 官方的说明 Declaring mockito dependency.
使用
至此,对于为什么要捕获调用参数我都觉得未完全叙述清楚。没关系可跳到下一节中从实现中进一步体会。
从面对被捕获参数
我们同样可以在打桩的时候捕获参数,如
上面代码得到错误信息
对于
JDK 自从 1.5 加入了泛型那已经是很多年前的事了,可如今 Mockito 的官方网站 http://site.mockito.org 的
以上代码放到现代 IDE 都会被警告的,虽然
给 UserService 增加一个方法 saveUsers(List<User> users)
下面的测试方法的确是可以工作
但是无法给
如果代码规范或 Maven 要求严格就必须给该测试方法加上
因为 Java 的泛型擦除的实现方式,所以从语法上下面的写法也是不合法的
好在 Mockito 也为我们考虑到了,还有 @Captor (Since 1.8.3) 注解,它像用 @Mock 声明 mock 对象一样声明捕获参数, 使用方法如下
和使用 Mockito 的其他注解一样,需要
相对来说用
相关链接:
永久链接 https://yanbin.blog/mockito-capture-method-paramters/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
为什么要捕获调用参数
在被 mocker 方法调用参数明确的情况下可无需捕获参数,例如,下面的情景: 1@Test
2public void dontCaptureArgument() {
3 UserDao userDao = Mockito.mock(UserDao.class);
4 UserService userService = new UserService(userDao);
5
6 User user = new User(1, "Yanbin");
7 userService.saveUser(user); //假如它的实现是 userDao.save(user)
8
9 verify(userDao, times(1)).save(user); //断言了 userDao.save(user) 操作的还是 user 对象
10}如果 UserService 的 save(user) 最终操作的不是同一个对象,它的实现稍加变化如下
1public class UserService {
2 private UserDao userDao;
3
4 public void save(User user) {
5 User revisedUser = new User(SequenceGenerator.next(), user.name);
6 userDao.save(revisedUser);
7 }
8} 由于在调用
userDao.save(revisedUser) 时的参数已不是传入 `userService.saveUser(user)` 的那个 user 对象了,所以继续用verify(userDao, times(1)).save(user);断言就要失败了,出现类似下面的信息
Argument(s) are different! Wanted:就是断言的 user 实现与 userDao 实际接受到的实例是不一样的。这时候我们可以选择懒堕一点,忽略参数的因数,只关心是否仅被调用一次,可写成
userDao.save(cc.unmi.User@6a2bcfcb);
-> at cc.unmi.UserServiceTest.captureNonGenericeArgument(UserServiceTest.java:25)
Actual invocation has different arguments:
userDao.save(cc.unmi.User@4de8b406);
-> at cc.unmi.UserService.saveUser(UserService.java:16)
Comparison Failure:
Expected :userDao.save(cc.unmi.User@6a2bcfcb);
Actual :userDao.save(cc.unmi.User@4de8b406);
1verify(userDao, times(1)).save(any(User.class));
2verify(userDao, times(1)).save(any()); //或懒到极致一点但用 any() 的话我们会有些缺乏安全感,太含糊,可能实际传递给
userDao 的的 user 对象根本不是我们想期望的。当然如果我们清楚知道
user.name 不会变的,我们可以用 `ArgumentMatcher` 来匹配参数1verify(userDao, times(1)).save(argThat(user1 -> user1.name.equals(user.name)));如果你所用的 mockito 版本不够高的话,`ArgumentMatcher` 可能还不是个 FunctionalInterface, 就得老老实实的用匿名类,
new ArgumentMatcher(){...}.注: 在写在此文时用的 mockito-core-2.6.2. 有些项目中或许仍在用 mockito-all 引入 mockito 依赖,这种方式无法引入 Mockito 2.*. 见 mockito 官方的说明 Declaring mockito dependency.
使用
argThat(ArgumentMatcher) 进行参数特征匹配也算是不错的方案,除此之外我们还有另一种办法,捕获参数后作事后断言。或是我们在给方法打桩时用 any() 参数,然后事后断言1when(userDao.findUserLike(any()).thenReturn(Mockito.mock(User.class)); //我们在后面的 verify() 中捕获 any() 所对应的的实际参数至此,对于为什么要捕获调用参数我都觉得未完全叙述清楚。没关系可跳到下一节中从实现中进一步体会。
如何捕获 mock 方法的调用参数
下面是一个简单的例子,只捕获一次调用的参数 1@Test
2public void captureNonGenericArgument() {
3 UserDao userDao = Mockito.mock(UserDao.class);
4 UserService userService = new UserService(userDao);
5
6 userService.saveUser(new User(null, "Yanbin"));
7
8 ArgumentCaptor<User> argumentCaptor = ArgumentCaptor.forClass(User.class);
9 verify(userDao, times(1)).save(argumentCaptor.capture());
10
11 assertEquals("Yanbin", argumentCaptor.getValue().name);
12 assertEquals("Chicago", argumentCator.getValue().city); //可断言捕获参数的更多特征
13}从面对被捕获参数
argumentCaptor.getValue() 的断言可看出它比 argThat() 的优势,argThat() 无法告诉我们不匹配的细节,下面是 argThat() 代码的测试失败信息1verify(userDao, times(1)).save(argThat(user -> user.name.equals("error")));Argument(s) are different! Wanted:而针对被捕获到的参数进行断言就自由多了,是 name 还是 city 属性不对从失败信息中会显示得清清楚楚。
userDao.save(
<User service test$$ lambda$ 1/ 2 0 8 7 8 8 5 3 9 7>
);
-> at cc.unmi.UserServiceTest.captureNonGenericeArgument(UserServiceTest.java:27)
Actual invocation has different arguments:
userDao.save(
cc.unmi.User@6c64cb25
);
-> at cc.unmi.UserService.saveUser(UserService.java:16)
我们同样可以在打桩的时候捕获参数,如
1ArgumentCaptor<User> argumentCaptor = argumentCaptor.forClass(User.class);
2when(userDao.findUserLike(argumentCaptor.capture)).thenReturn(Mockito.mock(User.class));
3
4assertEquals("Yanbin", argumentCaptor.getValue().name);捕获多次调用的参数
假如被 mock 方法会被调用多次,该如何知道每次调用时的参数呢?argumentCaptor.getValue() 只会返回最后一次调用的参数值 1@Test
2public void captureNonGenericeArgument() {
3 UserDao userDao = Mockito.mock(UserDao.class);
4 UserService userService = new UserService(userDao);
5
6 userService.saveUser(new User(null, "Yanbin"));
7 userService.saveUser(new User(null, "Unmi")); //这里手工模拟产生两次对 userDao.save(user) 的调用
8
9 ArgumentCaptor<User> argumentCaptor = ArgumentCaptor.forClass(User.class);
10 verify(userDao, times(2)).save(argumentCaptor.capture());
11
12 assertEquals("Yanbin", argumentCaptor.getValue().name);
13}上面代码得到错误信息
org.junit.ComparisonFailure:说明
Expected :Yanbin
Actual :Unmi
argumentCaptor.getValue() 仍然在工作,只是保存最近一次调用的参数值,而想要获得所有调用的参数值必须用 argumentCaptor.getAllValues(), 它返回一个 List<T>. 1@Test
2public void captureNonGenericeArgument() {
3 UserDao userDao = Mockito.mock(UserDao.class);
4 UserService userService = new UserService(userDao);
5
6 userService.saveUser(new User(null, "Yanbin"));
7 userService.saveUser(new User(null, "Unmi"));
8
9 ArgumentCaptor<User> argumentCaptor = ArgumentCaptor.forClass(User.class);
10 verify(userDao, times(2)).save(argumentCaptor.capture());
11
12 List<User> inputUsers = argumentCaptor.getAllValues();
13
14 assertEquals(2, inputUsers.size());
15 assertTrue(inputUsers.stream().allMatch(user ->
16 Arrays.asList("Yanbin", "Unmi").contains(user.name))
17 );
18}对于
argumentCaptor.getAllValues() 得到的 List<T> 可以按需进行断言,比如 mock 方法是异步无序调用的,则可以用 Stream 的 allMatch() 或 anyMatch() 去匹配断言。捕获带泛型的参数
原本写作此文的目的只作研究如何捕获带泛型的参数,没想从头至此说了那么内容才进入到初衷。JDK 自从 1.5 加入了泛型那已经是很多年前的事了,可如今 Mockito 的官方网站 http://site.mockito.org 的
How 里例子还是不为 List 声明具体类型的1List mockedList = mock(List.class);
2mockedList.add("one");
3verify(mockedList).add("one");以上代码放到现代 IDE 都会被警告的,虽然
List 是 List<Object> 的意思, 但 IDE 不希望你把 List<Object> 简化为 List。给 UserService 增加一个方法 saveUsers(List<User> users)
1public void saveUsers(List<User> users) {
2 userDao.save(users); //UserDao 中增加相应的 save(List<User> users) 方法
3}下面的测试方法的确是可以工作
1@Test
2public void captureGenericeArgument() {
3 UserDao userDao = Mockito.mock(UserDao.class);
4 UserService userService = new UserService(userDao);
5
6 userService.saveUsers(Collections.singletonList(new User(null, "Yanbin")));
7
8 ArgumentCaptor<List<User>> argumentCaptor = ArgumentCaptor.forClass(List.class);
9 verify(userDao, times(1)).save(argumentCaptor.capture());
10
11 assertEquals(1, argumentCaptor.getValue().size());
12 assertEquals("Yanbin", argumentCaptor.getValue().get(0).name);
13}但是无法给
ArgumentCaptor.forClass(List.class) 指定更内层的类型 User, 它的方法原型参数是 forClass(Class clazz), 所以代码行ArgumentCaptor<List<User>> argumentCaptor = ArgumentCaptor.forClass(List.class);有一个不安全类型赋值
如果代码规范或 Maven 要求严格就必须给该测试方法加上 @SuppressWarnings("unchecked") 注解来消除警告。还有别的办法呢?因为 Java 的泛型擦除的实现方式,所以从语法上下面的写法也是不合法的
1ArgumentCaptor<List<User>> argumentCaptor = ArgumentCaptor.forClass(List<User>.class); //这是错的好在 Mockito 也为我们考虑到了,还有 @Captor (Since 1.8.3) 注解,它像用 @Mock 声明 mock 对象一样声明捕获参数, 使用方法如下
1@RunWith(MockitoJUnitRunner.class)
2public class UserServiceTest {
3
4 @Captor
5 private ArgumentCaptor<List<User>> argumentCaptor;
6
7 @Test
8 public void captureGenericeArgument() {
9 UserDao userDao = Mockito.mock(UserDao.class);
10 UserService userService = new UserService(userDao);
11
12 userService.saveUsers(Collections.singletonList(new User(null, "Yanbin")));
13
14 verify(userDao, times(1)).save(argumentCaptor.capture());
15
16 assertEquals(1, argumentCaptor.getValue().size());
17 assertEquals("Yanbin", argumentCaptor.getValue().get(0).name);
18 }
19}和使用 Mockito 的其他注解一样,需要
@RunWith(MockitoJUnitRunner.class)指定 Junit Runner,或者是在运行测试前手工初始化,像
1@Before
2public void setup() {
3 MockitoAnnotations.initMocks(this);
4}相对来说用
@RunWith(MockitoJUnitRunner.class) 简单些,但 @RunWith 是独占的,比如在指定了其他 JUnit Runner(例如 @RunWith(SpringJUnit4ClassRunner.class)) 时,又要使用 Mockito 的注解,那我们就必须手工调用 MockitoAnnotations.initMocks(this) 了。相关链接:
永久链接 https://yanbin.blog/mockito-capture-method-paramters/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。