【问题标题】:Mockito verify + any behaves unpredictablyMockito verify + any 行为不可预测
【发布时间】:2015-03-19 02:28:42
【问题描述】:

我正在用 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 只是一个值类,用于从传入的 json 中收集所有参数。

EventProposalId

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

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

这就是 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 的错误还是应该这样?

【问题讨论】:

    标签: java testing mocking mockito spring-test


    【解决方案1】:

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

    当你写作时:

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

    ...Mockito 实际上匹配,就好像它是:

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

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

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

    【讨论】:

    • 感谢您的详细解答。它解释了很多。
    【解决方案2】:

    我认为错误在

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

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

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

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

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

    对我来说,看起来 any() 调用以某种方式导致 send() 匹配任何对象(可能是因为匹配器是“堆叠的”并且没有什么可以使用它?),即使它不是故意的。我写了一个显示类似行为的快速测试

    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
        }
    }
    

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

    【讨论】:

    • 我最终使用了 ArgumentCaptor + 自定义 HamcrestMatcher,但我仍然很好奇为什么该测试通过了。
    • 你说得对,它是一个实现细节。赞成,并感谢您的链接! :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-09
    • 2019-06-14
    相关资源
    最近更新 更多