Используйте Mockito, чтобы издеваться над некоторыми методами, но не с другими

Есть ли способ, используя 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);
}

Ответ 1

Чтобы прямо ответить на ваш вопрос, да, вы можете издеваться над одними методами, не насмехаясь над другими. Это называется частичной имитацией. Дополнительную информацию смотрите в документации 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

В этом случае каждая реализация метода является ложной, если не указано thenCallRealMethod() в предложении when(..).

Существует также возможность наоборот шпион вместо макет:

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(..).

Есть одна важная ошибка, когда вы используете 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 );

Это делегирует unstubbed вызовы для реальных реализаций.


Однако, с вашим примером, я считаю, что он все равно потерпит неудачу, поскольку реализация getValue() опирается на quantity и price, а не на 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);
}

Ответ 2

Частичное издевательство над классом также поддерживается Spy в mockito

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.19 и 2.7.22 docs для подробного объяснения.

Ответ 3

Согласно docs:

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();

Ответ 4

То, что вы хотите, это 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 does not... 
 * 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));
}

Документы значения 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.)
 */

Ответ 5

Частичное издевательство с использованием метода шпионки Mockito может быть решением вашей проблемы, как уже было сказано в ответах выше. В какой-то степени я согласен с тем, что для вашего конкретного варианта использования может быть более уместным издеваться над поиском БД. По моему опыту это не всегда возможно - по крайней мере, не без других обходных решений - что я считаю очень громоздким или, по крайней мере, хрупким. Обратите внимание, что частичное издевательство не работает со сторонними версиями Mockito. Вы используете как минимум 1,8.

Я бы просто написал простой комментарий для исходного вопроса вместо публикации этого ответа, но StackOverflow этого не допускает.

Еще одна вещь: я действительно не понимаю, что много раз задаваемый вопрос дает комментарий с "Почему вы хотите сделать это", не пытаясь хотя бы понять проблему. Особенно, когда речь заходит о необходимости частичного издевательств, действительно есть много вариантов использования, которые я могу себе представить, где это было бы полезно. Вот почему ребята из Mockito обеспечили эту функциональность. Разумеется, эту функцию нельзя переоценить. Но когда мы говорим о настройках тестовых случаев, которые иначе не могли быть установлены очень сложным образом, следует использовать шпионаж.