如何用Mockito ArgumentMatcher验证集合元素各被调用一次?
如何用Mockito验证方法被调用N次且每个元素恰好作为参数传入一次
场景说明
有这样一个方法,负责将元素列表中的每个元素添加到目标列表:
public void addElementsToList(List<String> theList, List<String> theElements) { for(String element : theElements) { theList.add(element); } }
需要验证:调用addElementsToList时,目标列表的add方法被每个元素恰好调用一次(不关心顺序)。
原测试代码及问题
原测试尝试用自定义ArgumentMatcher实现,但运行失败:
@Test public testAddElements() { List<String> list = mock(List.class); List<String> elements = Arrays.asList("a", "b", "c"); addElementsToList(list, elements); Mockito.verify(list, times(3)) .add(ArgumentMatchers.argThat(new ArgumentMatcher<String>() { Set<String> expected = new HashSet<>(elements); @Override public boolean matches(String argument) { return expected.remove(argument); } })); }
报错信息:
org.mockito.exceptions.verification.TooLittleActualInvocations: list.add(<custom argument matcher>); Wanted 3 times: -> at my.test.package.TestClass.testAddElements(TestClass.java:100) But was 0 times.
改成用contains检查时测试能通过,但无法防止同一参数被重复调用的情况。
正确实现方式
方法一:使用ArgumentCaptor捕获参数后验证
这是最直观可靠的方式,先捕获所有add方法的调用参数,再验证参数集合与预期完全一致(不关心顺序):
@Test public void testAddElements() { List<String> list = mock(List.class); List<String> elements = Arrays.asList("a", "b", "c"); addElementsToList(list, elements); // 创建字符串类型的参数捕获器 ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); // 捕获所有add方法的调用参数 Mockito.verify(list, times(elements.size())).add(captor.capture()); // 验证捕获的参数与预期元素完全匹配(顺序无关,次数一致) assertThat(captor.getAllValues(), containsInAnyOrder(elements.toArray())); }
这里containsInAnyOrder是Hamcrest匹配器,会严格校验两个集合的元素组成和出现次数,能有效避免重复调用的问题。
方法二:改进自定义ArgumentMatcher
原写法失败的核心原因是:Mockito会为每次调用检查创建新的ArgumentMatcher实例,导致内部的expected集合无法累计状态。要解决这个问题,需要把状态变量放到匹配器外部:
@Test public void testAddElements() { List<String> list = mock(List.class); List<String> elements = Arrays.asList("a", "b", "c"); Set<String> remainingElements = new HashSet<>(elements); addElementsToList(list, elements); Mockito.verify(list, times(elements.size())).add(ArgumentMatchers.argThat(argument -> { return remainingElements.remove(argument); })); // 最后验证所有元素都被匹配过,集合为空 assertTrue(remainingElements.isEmpty()); }
这种方式需要额外验证集合为空,确保没有元素被遗漏或重复调用。
方法三:使用Mockito的verifyEach(4.6+版本支持)
如果你的Mockito版本在4.6及以上,可以用verifyEach简化验证逻辑:
@Test public void testAddElements() { List<String> list = mock(List.class); List<String> elements = new ArrayList<>(Arrays.asList("a", "b", "c")); addElementsToList(list, elements); // 验证add方法被调用指定次数,且参数恰好是预期元素 Mockito.verify(list, times(elements.size())) .add(Mockito.argThat(arg -> elements.remove(arg))); // 确认所有元素都被匹配 assertTrue(elements.isEmpty()); }
注意这里要用可修改的ArrayList,不能用Arrays.asList返回的不可修改列表。
原代码失败原因解释
你最初的自定义ArgumentMatcher中,expected集合定义在匹配器内部,Mockito每次检查调用时都会新建一个匹配器实例,导致每次matches调用都是基于全新的集合,remove操作无法累计状态。三次调用中没有一次能匹配到“剩余元素被移除”的条件,因此报错“调用次数为0”。
内容的提问来源于stack exchange,提问作者geronimo
相关产品推荐
相关产品推荐

