概述

在这个教程中,我们将学习如何使用ArgumentMatcher,并讨论它与ArgumentCaptor的区别。

对于Mockito框架的入门介绍,请参阅这篇教程

2. Maven依赖项

我们需要添加一个单独的依赖:

<dependency>
    <groupId>org.mockito</groupId> 
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version> 
    <scope>test</scope>
</dependency>

Mockito的最新版本可以在Maven中央仓库找到。

3. ArgumentMatchers

我们可以以多种方式配置mocked方法。一种选择是返回固定值:

doReturn("Flower").when(flowerService).analyze("poppy");

在上面的例子中,只有当FlowerServiceanalyze方法接收到字符串"poppy"时,才会返回字符串"Flower"。

然而,有时可能需要对更广泛的值或未知值做出响应。

在这种情况下,我们可以通过参数匹配器来配置我们的mocked方法:

when(flowerService.analyze(anyString())).thenReturn("Flower");

现在,由于使用了anyString参数匹配器,无论我们传递什么值给analyze方法,结果都会相同。ArgumentMatchers使我们能够灵活地进行验证或模拟。

如果一个方法有多个参数,我们不能只对其中一些参数使用ArgumentMatchers。Mockito要求我们为所有参数提供匹配器或确切的值。

以下是一个不正确的示例:

when(flowerService.isABigFlower("poppy", anyInt())).thenReturn(true);

我们可以通过运行下面的测试来验证这一点:

assertThrows(InvalidUseOfMatchersException.class, 
    () -> when(flowerService.isABigFlower("poppy", anyInt())).thenReturn(true));

要解决这个问题,并保持字符串名称"poppy",我们将使用eq matcher

when(flowerService.isABigFlower(eq("poppy"), anyInt())).thenReturn(true);

让我们运行测试来确认这一点:

when(flowerService.isABigFlower(eq("poppy"), anyInt())).thenReturn(true);

Flower flower = new Flower("poppy", 15);

Boolean response = flowerController.isABigFlower(flower);
assertThat(response).isTrue();

当我们使用匹配器时,还有两点需要注意:

  • 我们不能用它们作为返回值;在模拟调用时,我们需要确切的值。
  • 我们不能在验证或模拟之外使用参数匹配器。

根据第二点,Mockito会检测到参数放置不当,并抛出InvalidUseOfMatchersException异常。

一个不好的例子是:

flowerController.isAFlower("poppy");

String orMatcher = or(eq("poppy"), endsWith("y"));
assertThrows(InvalidUseOfMatchersException.class, () -> verify(flowerService).analyze(orMatcher));

实现上述代码的方式应该是:

verify(flowerService).analyze(or(eq("poppy"), endsWith("y")));

Mockito还提供了AdditionalMatchers,用于在匹配原始类型和非原始类型参数的ArgumentMatchers上执行常见的逻辑操作(如'not'、'and'、'or')。

4. 定制参数匹配器

创建我们自己的匹配器允许我们在特定场景中选择最佳方法,从而编写出高质量、易于维护的测试。

例如,我们有一个MessageController,负责发送消息。它会接收一个MessageDTO,然后从中创建一个Message,由MessageService发送。

我们的验证将很简单:我们只需确认我们精确地向MessageService调用了1次,传入任何Message:

MessageDTO messageDTO = new MessageDTO();
messageDTO.setFrom("me");
messageDTO.setTo("you");
messageDTO.setText("Hello, you!");

messageController.createMessage(messageDTO);

verify(messageService, times(1)).deliverMessage(any(Message.class));

由于Message是在被测试的方法内部构造的,我们必须使用any作为匹配器。

这种方法不允许我们验证Message内的数据,这可能与MessageDTO内的数据不同。

因此,我们将实现一个自定义参数匹配器:

public class MessageMatcher implements ArgumentMatcher<Message> {

    private Message left;

    // constructors

    @Override
    public boolean matches(Message right) {
        return left.getFrom().equals(right.getFrom()) &&
          left.getTo().equals(right.getTo()) &&
          left.getText().equals(right.getText()) &&
          right.getDate() != null &&
          right.getId() != null;
    }
}

为了使用我们的匹配器,我们需要修改测试,将any替换为argThat:

MessageDTO messageDTO = new MessageDTO();
messageDTO.setFrom("me");
messageDTO.setTo("you");
messageDTO.setText("Hello, you!");

messageController.createMessage(messageDTO);

Message message = new Message();
message.setFrom("me");
message.setTo("you");
message.setText("Hello, you!");

verify(messageService, times(1)).deliverMessage(argThat(new MessageMatcher(message)));

现在我们知道我们的Message实例将具有与MessageDTO相同的数据。

5. 自定义参数匹配器与ArgumentCaptor

两种技术,即custom argument matchersArgumentCaptor,都可以用来确保某些参数传递给mock。

但是,如果需要对参数值进行进一步的断言以完成验证,或者我们的custom argument matcher不太可能重用,那么ArgumentCaptor可能更适合。

通常,通过ArgumentMatcher实现的自定义参数匹配器更适合于模拟。

6. 总结

在这篇文章中,我们探讨了Mockito中的ArgumentMatcher功能。还讨论了它与ArgumentCaptor的不同之处。

如往常一样,所有示例的完整源代码可在GitHub上找到。