【问题标题】:Mockito: method's return value depends on other method calledMockito:方法的返回值取决于调用的其他方法
【发布时间】:2012-09-29 21:04:21
【问题描述】:

在我的单元测试中,我需要模拟一个在不同方法中具有nextItem()isEmpty() 方法的接口:

public interface MyQueue {
    Item nextItem();
    boolean isEmpty();
    //other methods
    ...
}

我对模拟的要求是 isEmpty() 最初应该返回 false,但在 nextItem() 被调用之后 isEmpty() 应该返回 true。因此,我正在用一个项目模拟一个队列。

  1. 用 mockito 实现这种模拟的最简单方法是什么?
  2. 能否实现附加要求:第二次、第三次调用nextItem() 等等会导致特定类型的异常?

附:我不想为测试提供我的接口的完整实现,因为其中有其他方法,导致代码难以理解和冗长。

【问题讨论】:

  • 你可以有一个布尔变量,当你输入 nextItem() 时你可以切换为 true
  • @RohitJain 你能建议不冗长的实现吗?

标签: java junit mockito


【解决方案1】:

您可以使用 thenAnswer() 来实现,Mockito 文档认为这是一个有争议的功能:

Mockito 最初并未包含的另一个有争议的功能。我们建议仅使用带有 toReturn() 或 toThrow() 的简单存根。这两个应该足以测试/试驾任何干净简单的代码。

下面是答案:

private boolean called = false;

when(mock.nextItem()).thenAnswer(new Answer() {
 Object answer(InvocationOnMock invocation) {   
     called = true;       
     return item;
 }
when(mock.isEmpty()).thenAnswer(new Answer() {
 Object answer(InvocationOnMock invocation) {          
     return called;
 }
});

【讨论】:

  • 哇!谢谢,到目前为止,这是我问题的最佳解决方案 - 代码简短易读。我必须注意,在我实现您的想法时,Answers 返回特定类型(Item 和 Boolean 而不是 Object)。这种方法的一个小问题 - 我们必须引入测试类字段called,它仅用于一种测试方法。我们不能将called 的声明放在测试方法中,因为它需要是最终的。
【解决方案2】:

这是一个简单的例子:

//given
MyQueue mock = mock(MyQueue.class);

given(mock.isEmpty()).willReturn(false, true);
given(mock.nextItem()).willReturn(someItem);

//when
mock.isEmpty();   //yields false
mock.nextItem();  //yields someItem
mock.isEmpty();   //yields true

//then
InOrder inOrder = inOrder(mock);
inOrder.verify(mock).isEmpty();
inOrder.verify(mock).nextItem();
inOrder.verify(mock).isEmpty();

willReturn(false, true) 表示:在第一次调用时返回 false,在第二次调用时返回 trueInOrder 对象用于验证调用顺序。更改顺序或删除nextItem() 调用,测试将失败。

您也可以使用以下语法:

given(mock.isEmpty()).
        willReturn(false).
        willReturn(true).
        willThrow(SpecialException.class);

如果需要更强大的 mocking 语义,可以引入重炮——自定义应答回调:

given(mock.isEmpty()).willAnswer(new Answer<Boolean>() {
    private int counter = 0;
    @Override
    public Boolean answer(InvocationOnMock invocation) throws Throwable {
        switch(++counter) {
            case 1: return false;
            case 2: return true;
            default: throw new SpecialException();
        }
    }
});

但这很容易导致无法维护的测试代码,谨慎使用。

最后你可以spy your real object 只模拟选定的方法。

【讨论】:

  • 谢谢,很好的解释。但是您的解决方案对模拟提出了更严格的要求:您要求在调用 nextItem() 之前不会多次调用 isEmpty()
  • 可悲的是,即使是重型火炮也不会强制方法之间的交互 ​​- 要求分别施加在每个方法上,并且仅基于多次调用...
  • 在下面查看我的解决方案,但是如果您使用依赖于另一个答案的答案,那么重型火炮可以强制执行此类操作。然后,它可以根据之前可能调用或未调用过的其他事情做出决定。
  • @pavel_kazlou:好吧,您可以对这两种方法使用相同的答案对象。你也可以回答共享一些全局状态的对象。但这变得很难贴在这里......
  • 请注意,如果您不想使用 BDDMockito 方法,可以将其写为when(mock.isEmpty()).thenReturn(false, true);。另请注意,isEmpty() 的第三次和任何后续调用也将返回 true - 当您在 thenReturnwillReturn 之后列出多个返回值时,您列出的最后一个值可能会重复返回。
【解决方案3】:

您可以提供一些自定义 Answer 实现,其中一个依赖于另一个:

public class NextItemAnswer implements Answer<Item> {
   private int invocationCount = 0;
   private Item item;
   public NextItemAnswer(Item item) {
       this.item = item;
   }

   public Item answer(InvocationOnMock invocation) throws Throwable {
       invocationCount++;
       return item;
   }

   public int getInvocationCount() {
       return invocationCount;
   }
}

public class IsEmptyAnswer implements Answer<Boolean> {
   private NextItemAnswer nextItemAnswer;
   public IsEmptyAnswer(NextItemAnswer nextItemAnswer) {
       this.nextItemAnswer = nextItemAnswer;
   }
   public Boolean answer(InvocationOnMock invocation) throws Throwable {
       return nextItemAnswer.getInvocationCount() >= 0;
   }
}

然后使用它:

NextItemAnswer nextItemAnswer = new NextItemAnswer(item);
IsEmptyAnswer isEmptyAnswer = new IsEmptyAnswer(nextItemAnswer);

when(mock.isEmpty()).thenAnswer(isEmptyAnswer);
when(mock.nextItem()).thenAnswer(nextItemAnswer);

你可能会调整,因为我没有测试过这段代码,但方法应该是你需要的。

【讨论】:

  • 谢谢,现在我可以看到这实际上可以用经典 java 风格的 Answers 来完成。很遗憾,它很冗长,将我最初的想法隐藏在许多代码行之后。
  • 你可以通过使用匿名类而不是声明类来稍微简化它,但想法仍然相同。
【解决方案4】:

我确实意识到您明确写过您不想提供 MyQueue 的完整实现,但老实说,这将是我要做的第一件事。

事实上,为了使测试更容易测试,我经常提供相当复杂的接口/对象的“模拟”实现。我不是唯一一个这样认为的人:例如,Spring Framework 提供了许多复杂对象的模拟版本(MockHttpServletRequest、MockHttpServletResponse 等)。

在这种情况下,我会避免弄乱我的测试,并在单独的包中甚至在生产代码中提供此类。

MockQueue 将使您的测试比此处给出的其他(但正确的)响应更具可读性。

【讨论】:

  • 确实,这是最好的答案。其他的没有错,但是他们没有意识到用户需要的只是接口的部分实现,逻辑很简单。在 JMockit 中,有一个 API 适合这种情况:MyQueue mock = new MockUp&lt;MyQueue&gt;() { boolean nextItemCalled; @Mock boolean isEmpty() { return nextItemCalled; } @Mock Item nextItem() { nextItemCalled = true; return someItem; } }.getMockInstance();
  • 如果必须针对不同的测试更改实现怎么办?然后,您必须为实现引入一些带有继承或组合的 OO 设计。我认为只模拟每个测试所需的方法使单元测试简单易懂。我建立了我的测试类应该遵守的合同,应该避免不相关的代码。顺便说一句,感谢您提供这种涉及概念讨论的答案,我对您的建议非常感兴趣,但目前无法理解其中的好处。
  • 好吧,你会模拟Collection 接口吗?我见过有人这样做,但这对我来说听起来不是一个好主意。名为Queue 的接口为接口的客户端提供了一些基本假设。即使实现可能不同,从客户端类的角度来看,一般行为也应该或多或少相同。你有没有更具体的例子说明这种情况不起作用?
  • @Eric mocking Collection 不是一个好主意,因为它有很多 经过良好测试 的实现,我可以可靠地使用它们来形成需求。相反的情况是当我需要使用 my own 类/接口作为测试类的依赖项时。如果我在测试中使用我自己的完整实现,那意味着我不是孤立地测试目标类,而是与队列依赖一起测试。这意味着一个成功的测试可以被破坏,或者(更糟糕的是)失败的测试可以通过改变代码而不是在测试类中而是在它的依赖项中来修复。
  • 当使用你自己的实现时,你有两个选择:要么为你的测试提供一个“太简单而无法破解”的实现(基本上就是 MockQueue),并且测试它应该不是真的必要。或者,您只是重用一个实际的、有效的和经过测试的实现。实际上,在我当前的项目中,我确实有这种对象。一个 DataHistory 类,相当复杂,因此经过了彻底的测试,我在其他测试中重用了它。我认为它之所以有效,是因为它的行为相当容易理解(添加东西,按特定顺序返回它们),真的很像一个集合。
【解决方案5】:

您可以使用the mockito docs 中描述的技术告诉 mockito 在对同一模拟方法的连续调用时做出不同的回答。

when(mock.isEmpty())
  .thenReturn(false)
  .thenReturn(true);

将使isEmpty() 调用仅在第一次调用时返回 true,并且

when(mock.nextItem())
  .thenReturn(item)
  .thenThrow(new NextOnEmptyQueueException())

将使nextItem() 在第一次调用时返回某些内容,并在以后的调用中抛出异常。

我不知道是否有可能使其中一种方法的结果取决于对另一种方法的调用顺序。如果确实有可能,我敢肯定它要复杂得多。

【讨论】:

  • 谢谢(特别是对于我的第二个问题的良好链接解决方案)。但是与 Tomasz 解决方案相同的问题仍然存在 - 您的解决方案意味着 isEmpty() 在调用 nextItem() 之前仅被调用一次。
  • 是的,确实如此,但正如答案中所述,我不确定是否可以做得更好。
【解决方案6】:

你可以制作一个实用方法,然后在任何你想要的地方使用它。

public static boolean mockHasInvocation(Object mock, String methodName, Object... args) {
    return mockingDetails(mock).getInvocations().stream()
            .anyMatch(o -> o.getMethod().getName().equals(methodName) && Arrays.equals(o.getArguments(), args));
}

简单用法:

if(mockHasInvocation(mockObject, "methodName", "argument1", "argument2")){doSomething();}

在这种情况下,您不需要任何额外的变量,它更像是“Mockito 风格”。

【讨论】:

    猜你喜欢
    • 2014-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多