使用Mockito模拟一些方法但不是所有方法

609

使用Mockito,有没有一种方法可以模拟类中的某些方法,而不是其他方法?

例如,在这个(虽然牵强附会的)Stock类中,我想要模拟getPrice()getQuantity()的返回值(如下面的测试片段所示),但我希望getValue()按照Stock类中编写的乘法执行。

public class Stock {
  private final double price;
  private final int quantity;

  Stock(double price, int quantity) {
    this.price = price;
    this.quantity = quantity;
  }

  public double getPrice() {
    return price;
  }

  public int getQuantity() {
    return quantity;
  }
  public double getValue() {
    return getPrice() * getQuantity();
  }

  @Test
  public void getValueTest() {
    Stock stock = mock(Stock.class);
    when(stock.getPrice()).thenReturn(100.00);
    when(stock.getQuantity()).thenReturn(200);
    double value = stock.getValue();
    // Unfortunately the following assert fails, because the mock Stock getValue() method does not perform the Stock.getValue() calculation code.
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}

4
为什么你想这样做?你应该在测试类的时候进行测试(在这种情况下,根本不应该进行模拟),或者在测试另一个类时进行模拟(在这种情况下,没有功能)。为什么要进行部分模拟? - weltraumpirat
4
好的,这是一个真实情况的小例子。实际上,我试图避免调用数据库,通过传递编造的值,但我想验证其他方法是否能正确处理这些编造的值。有更好的方法吗? - Victor Grazi
6
当然:将数据库调用移至单独的类中(领域逻辑和数据库访问不应在同一类中;它们是两个不同的关注点),提取其接口,使用该接口从领域逻辑类连接,并只在测试期间模拟接口。 - weltraumpirat
1
我完全同意,如果不上传大量代码(包括第三方库),很难解释清楚整个情况。 - Victor Grazi
1
你可能可以这样做。但那不会是“更好的方法”:你的数据库代码是一个实现细节,你希望将其隐藏在应用程序的其余部分中,甚至可能移动到不同的包中。你不想每次更改 sequel 语句时都必须重新编译领域逻辑,对吧? - weltraumpirat
显示剩余4条评论
5个回答

965

直接回答您的问题,是的,您可以模拟一些方法而不模拟其他方法。这被称为部分模拟。请参见Mockito部分模拟文档以获取更多信息。

对于您的示例,您可以在测试中执行以下操作:

Stock stock = mock(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
when(stock.getValue()).thenCallRealMethod();  // Real implementation

在这种情况下,除非在when(..)子句中指定thenCallRealMethod(),否则每个方法实现都被模拟。

还有一种可能是使用spy而不是mock

Stock stock = spy(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
// All other method call will use the real implementations
在这种情况下,除非你使用 when(..) 定义了一个模拟行为,否则所有方法实现都是真实的。
当你像前面的例子一样在 spy 中使用 when(Object) 时,有一个重要的陷阱。真实方法将被调用(因为在运行时评估stock.getPrice()之前就已经评估了 when(..))。如果你的方法包含不应该被调用的逻辑,这可能会造成问题。你可以像这样编写前面的示例:
Stock stock = spy(Stock.class);
doReturn(100.00).when(stock).getPrice();    // Mock implementation
doReturn(200).when(stock).getQuantity();    // Mock implementation
// All other method call will use the real implementations

另一种可能性是使用 org.mockito.Mockito.CALLS_REAL_METHODS,例如:


Stock MOCK_STOCK = Mockito.mock( Stock.class, CALLS_REAL_METHODS );

这将委托未存根的调用到真实的实现。


但是,根据您的示例,我相信它仍将失败,因为getValue() 的实现依赖于quantityprice,而不是您已经模拟的getQuantity()getPrice()

另一个可能性是完全避免使用模拟:

@Test
public void getValueTest() {
    Stock stock = new Stock(100.00, 200);
    double value = stock.getValue();
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}

43
我认为这个答案是错误的。你需要监视对象的一个实例,而不是模拟整个类。 - GaRRaPeTa
4
我会说"观察"和"嘲笑"都是合理的替代词。由于原帖中指出这只是一个简化的例子,所以很难确定哪个是最好的选择。 - Jon Newmuis
3
截止2016年9月,public static <T> T spy(java.lang.Class<T> classToSpy) 方法仍然带有 @Incubating 注解... 如果你真的想要使用部分模拟功能,则应在实例上调用 spy 方法。 - mike rodent
5
Stock stock = spy(Stock.class); 这看起来不对,spy方法似乎只接受对象而不是类。 翻译:这段代码似乎有误,spy方法似乎只接收对象而非类。 - Paramvir Singh Karwal
17
感谢您指出doReturn(retval).when(spyObj).methodName(args)when(spyObj.methodName(args)).thenReturn(retval)之间的区别,这对我们非常有帮助。 - Captain_Obvious
显示剩余9条评论

175

Mockito也支持通过Spy来对类进行部分模拟

List list = new LinkedList();
List spy = spy(list);

//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);

//using the spy calls real methods
spy.add("one");
spy.add("two");

//size() method was stubbed - 100 is printed
System.out.println(spy.size());

请查看1.10.192.7.22文档以获取详细说明。


47
根据 文档
Foo mock = mock(Foo.class, CALLS_REAL_METHODS);

// this calls the real implementation of Foo.getSomething()
value = mock.getSomething();

when(mock.getSomething()).thenReturn(fakeValue);

// now fakeValue is returned
value = mock.getSomething();

2
感谢您演示如何设置模拟,其中真实实现被调用以处理除测试中需要控制的少数方法之外的所有方法。 - bigh_29
class NaughtyLinkedList extends LinkedList { public int size() { throw new RuntimeException("不要调用我");} }@Test public void partialMockNaughtLinkedList(){ List mock = mock(NaughtyLinkedList.class, CALLS_REAL_METHODS); mock.add(new Object()); // 这个调用了实际函数 when(mock.size()).thenReturn(2); // 不知道为什么,这一行会抛出运行时异常 assertEquals(2,mock.size()); } 无法正常工作。由于某些原因,在执行“when”时,实际上会执行本应被模拟的方法。 - Lance Kind
6
问题在于“何时”执行你想要部分模拟的事务。为了避免这种情况,有一个替代方法:使用doReturn()。请参阅http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/Mockito.html#spy中的doReturn()。 - Lance Kind
2
尝试实现这个答案时发现 when(mock.getSomething()).thenReturn(fakeValue); 不起作用,而必须使用 doReturn(fakeValue).when(mock).getSomething() 才行。 - AdityaKapreShrewsburyBoston

21
您需要的是根据文档 org.mockito.Mockito.CALLS_REAL_METHODS
/**
 * Optional <code>Answer</code> to be used with {@link Mockito#mock(Class, Answer)}
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations.
 * <p>
 * This implementation can be helpful when working with legacy code.
 * When this implementation is used, unstubbed methods will delegate to the real implementation.
 * This is a way to create a partial mock object that calls real methods by default.
 * <p>
 * As usual you are going to read <b>the partial mock warning</b>:
 * Object oriented programming is more less tackling complexity by dividing the complexity into separate, specific, SRPy objects.
 * How does partial mock fit into this paradigm? Well, it just doesn't... 
 * Partial mock usually means that the complexity has been moved to a different method on the same object.
 * In most cases, this is not the way you want to design your application.
 * <p>
 * However, there are rare cases when partial mocks come handy: 
 * dealing with code you cannot change easily (3rd party interfaces, interim refactoring of legacy code etc.)
 * However, I wouldn't use partial mocks for new, test-driven & well-designed code.
 * <p>
 * Example:
 * <pre class="code"><code class="java">
 * Foo mock = mock(Foo.class, CALLS_REAL_METHODS);
 *
 * // this calls the real implementation of Foo.getSomething()
 * value = mock.getSomething();
 *
 * when(mock.getSomething()).thenReturn(fakeValue);
 *
 * // now fakeValue is returned
 * value = mock.getSomething();
 * </code></pre>
 */

因此,您的代码应如下所示:
import org.junit.Test;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

public class StockTest {

    public class Stock {
        private final double price;
        private final int quantity;

        Stock(double price, int quantity) {
            this.price = price;
            this.quantity = quantity;
        }

        public double getPrice() {
            return price;
        }

        public int getQuantity() {
            return quantity;
        }

        public double getValue() {
            return getPrice() * getQuantity();
        }
    }

    @Test
    public void getValueTest() {
        Stock stock = mock(Stock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        when(stock.getPrice()).thenReturn(100.00);
        when(stock.getQuantity()).thenReturn(200);
        double value = stock.getValue();

        assertEquals("Stock value not correct", 100.00 * 200, value, .00001);
    }
}

Stock stock = mock(Stock.class);的调用会调用org.mockito.Mockito.mock(Class<T>),其代码如下:

 public static <T> T mock(Class<T> classToMock) {
    return mock(classToMock, withSettings().defaultAnswer(RETURNS_DEFAULTS));
}
< p> RETURNS_DEFAULTS 的文档说明:
/**
 * The default <code>Answer</code> of every mock <b>if</b> the mock was not stubbed.
 * Typically it just returns some empty value. 
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations. 
 * <p>
 * This implementation first tries the global configuration. 
 * If there is no global configuration then it uses {@link ReturnsEmptyValues} (returns zeros, empty collections, nulls, etc.)
 */

1
很好地发现了...但我能问一下为什么要像这样使用withSettings()...吗?似乎org.mockito.internal.stubbing.answers.CallsRealMethods()(例如)可能能够完成任务...并且此类的javadoc明确表示它用于部分mocks的使用... - mike rodent
3
另外...这难道不会遇到其他回答中遇到过的问题吗:即thenReturn实际上会执行该方法(虽然在此示例中可能不会出现问题),因此在这种情况下最好使用doReturn...? - mike rodent

3
使用Mockito的spy方法进行部分模拟可能是您的问题的解决方案,如上面的答案所述。在某种程度上,对于您具体的用例,我认为模拟DB查找可能更合适。根据我的经验,这并不总是可能的 - 至少没有其他解决方法 - 我认为这非常麻烦或至少很脆弱。请注意,部分模拟不适用于所有版本的Mockito。您必须使用至少1.8.0。
对于原始问题,我只会写一个简单的评论而不是发布此答案,但是StackOverflow不允许这样做。
最后:我真的无法理解为什么在这里提出问题的许多次数都会得到“为什么要这样做”的评论,而不至少尝试理解问题。特别是当涉及到部分模拟的需求时,我可以想象有很多用例它会很有用。这就是为什么Mockito的人们提供了这个功能。当然,不应过度使用此功能。但是,当我们谈论否则无法以非常复杂的方式建立的测试用例设置时,应该使用Spying。

3
我觉得这个答案在某种程度上是个人意见。请考虑进行编辑。 - soundslikeodd
2
点赞以鼓励家庭中的新成员。不必将其置于负面区域,因为没有真正的技术错误或不正确的语言/语气。对新成员要友善。谢谢。 - Saurabh Patil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接