Mockito verify + any 行为不可预测

Mockito verify + any behaves unpredictably

我正在 Spring MVC + axon 中为我的控制器编写集成测试。

我的控制器是一个简单的 RestController,有一个方法:

@RequestMapping(value = "/", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public void createEventProposal(@RequestBody CreateEventProposalForm form) {
    CreateEventProposalCommand command = new CreateEventProposalCommand(
            new EventProposalId(),
            form.getName(),
            EventDescription.of(form.getDescription()),
            form.getMinimalInterestThreshold());

    commandGateway.send(command);
}

CreateEventProposalForm 只是一个值 class,用于从传入 json 收集所有参数。

EventProposalId

是另一个值对象,代表一个标识符。它可以基于字符串构建,也可以不带任何参数——在后一种情况下,会生成一个 UUID。

现在,我想编写一个测试用例,给定一个适当的 json 我的控制器应该使用适当的命令对象在我的命令网关模拟上调用发送方法。

这是 mockito 表现得有点出乎意料的时候:

@Test
public void givenPostRequestShouldSendAppropriateCommandViaCommandHandler() throws Exception {

    final String jsonString = asJsonString(
            new CreateEventProposalForm(eventProposalName, eventDescription, minimalInterestThreshold)
    );

    mockMvc.perform(
            post(URL_PATH)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(jsonString)
    );

    verify(commandGatewayMock, times(1))
            .send(
                            new CreateEventProposalCommand(
                                    any(EventProposalId.class),
                                    eventProposalName,
                                    EventDescription.of(eventDescription),
                                    minimalInterestThreshold
                            )
            );


}

如果我将 EventProposalId 的新实例传递给 EventProposalCommand 构造函数,请说:

new CreateEventProposalCommand(
            EventProposalId.of("anId"),
            eventProposalName,
            EventDescription.of(eventDescription),
            minimalInterestThreshold
    )

正如你所料,它失败了。 但是给定 any(EventProposalId.class) 相反,我可以传递完全虚拟的值,例如

new CreateEventProposalCommand(
            any(EventProposalId.class),
            "dummy name",
            EventDescription.of("dummy description"),
            666
    )

作为其他参数,测试总是通过。

如果没有方法参数拦截,我怎么能做出这样的断言呢? 这是 mockito 的错误还是应该这样?

我认为错误在

    verify(commandGatewayMock, times(1))
        .send(
                        new CreateEventProposalCommand(
                                any(EventProposalId.class),
                                eventProposalName,
                                EventDescription.of(eventDescription),
                                minimalInterestThreshold
                        )
        );

您实际上是在创建一个新的 CreateEventProposalCommand 对象,然后将其传递给 Mockito。 Mockito 没有拦截构造函数参数,因此它不能使用它们。在这种情况下,any(EventProposalId.class) 只是 returns null。您可以在发送参数中使用匹配器,例如

verify(commandGatewayMock, times(1).send(any(CreateEventProposalCommand.class))

但这当然不符合您的要求。

问题仍然存在:为什么测试总是通过?我认为这可能是 Mockito 匹配器的一个实现细节,这里有描述 How do Mockito matchers work?

对我来说,似乎 any() 调用以某种方式导致 send() 匹配任何对象(也许是因为匹配器是 "stacked" 并且没有什么可以使用它?),即使它不是为了。我写了一个显示类似行为的快速测试

import org.mockito.Mockito;

public class MockitoTest {

    public void onOuter(Outer outer) {
    }

    public static class Outer {
        private Inner inner;

        public Outer(Inner inner) {
            this.inner = inner;
        }

    }

    public static class Inner {

    }

    public static void main(String[] args) {
        MockitoTest mockitoTest = Mockito.mock(MockitoTest.class);
        mockitoTest.onOuter(new Outer(new Inner()));
        Mockito.verify(mockitoTest)
                .onOuter(new Outer(Mockito.any(Inner.class))); // passes but shouldn't
        Mockito.verify(mockitoTest).onOuter(new Outer(new Inner())); // fails
    }
}

不幸的是,我不知道实现您想要实现的目标的最简单方法是什么。

为了扩展 ,它通过了,因为你 巧合 在模拟上匹配单参数方法时使用一个匹配器,这就是行为的原因不一致。

当你写:

verify(commandGatewayMock, times(1))
            .send(
                            new CreateEventProposalCommand(
                                    any(EventProposalId.class),
                                    eventProposalName,
                                    EventDescription.of(eventDescription),
                                    minimalInterestThreshold
                            )
            );

...Mockito 实际上匹配为:

verify(commandGatewayMock, times(1))
        .send(any());

any 的调用不会 return 匹配任何对象的特殊对象实例;相反,它 returns null 并告诉 Mockito 跳过匹配某个参数。如果您在存根或验证中使用任何匹配器,这就是您通常需要对所有参数使用匹配器的部分原因:匹配器和参数必须一对一排列,而 Mockito 不够智能,无法深入使用匹配器(即在对 new CreateEventProposalCommand 的调用中)。

在这种情况下,Mockito 在堆栈上看到一个 any 匹配器(any(EventProposalId.class)any 上的参数只是为了帮助 javac 找出泛型)和验证一个参数方法 (commandGatewayMock.send),并错误地假设两者一起使用——这会导致您的测试通过,而不管您的 CreateEventProposalCommand 构造函数的参数如何。