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

402

Есть ли способ, используя 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
3
Хорошо, это маленький пример из реальной жизни. На самом деле я пытаюсь избежать обращения к базе данных, передавая искусственные значения, но я хочу убедиться, что другие методы работают правильно с этими искусственными значениями. Есть лучший способ сделать это?
Виктор Граци
5
Конечно: переместите вызовы вашей базы данных в отдельный класс (логика домена и доступ к базе данных не должны быть в одном классе; это две разные проблемы), извлеките его интерфейс, используйте этот интерфейс для соединения из класса логики домена и имитируйте только интерфейс во время тестирования.
Weltraumpirat
1
Я полностью согласен, трудно объяснить всю картину, не загружая здесь куски кода, включая сторонние библиотеки.
Виктор Граци
1
Вы, вероятно, могли бы. Но тогда это не будет «лучшим способом сделать это»: код вашей базы данных - это деталь реализации, которую вы хотите скрыть от остальной части вашего приложения, возможно, даже перейти к другому пакету. Вы бы не хотели перекомпилировать свою доменную логику каждый раз, когда вы меняете оператор сиквела, не так ли?
Weltraumpirat

Ответы:

644

Чтобы прямо ответить на ваш вопрос, да, вы можете издеваться над одними методами, не насмехаясь над другими. Это называется частичной имитацией . См. Документацию 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);
}
Джон Ньюмуис
источник
21
Я думаю, что этот ответ неверен. Вам нужно шпионить экземпляр объекта, а не MOCK класса.
GaRRaPeTa
2
@GaRRaPeTa Я бы сказал, что шпионаж и издевательство - разумные альтернативы. Трудно сказать, какой из них лучше всего подходит для этого случая, поскольку ОП утверждает, что это упрощенный пример.
Джон Ньюмуис
1
Не должен ли это быть «Шпион» вместо «Шутка», так как «Шпион» лучше обеспечит частичную насмешку.
Тарун Сапра
2
Stock stock = spy(Stock.class);Это кажется неправильным, spyметод, кажется, принимает только объекты, а не классы.
Парамвир Сингх Карвал
4
+1 за указание на разницу между doReturn(retval).when(spyObj).methodName(args)иwhen(spyObj.methodName(args)).thenReturn(retval)
Captain_Obvious
140

Частичное издевательство над классом также поддерживается через шпион в 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документы для подробного объяснения.

Sudarshan
источник
37

Согласно документам :

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("don't call me");} } @Test public void partialMockNaughtLinkedList(){ List mock = mock(NaughtyLinkedList.class, CALLS_REAL_METHODS); mock.add(new Object()); // this calls the real function when(mock.size()).thenReturn(2); // For whatever reason, this lines throws the RuntimeException. assertEquals(2,mock.size()); }Это не работает По какой-то причине, когда «когда» выполняется, он фактически выполняет метод, который должен быть смоделирован. Код:
Ланс Добрый
3
Проблема в том, «когда». «Когда» фактически выполнит то, что вы хотите частично высмеять. Чтобы избежать этого, есть альтернатива: doReturn (). См. DoReturn () по адресу docs.mockito.googlecode.com/hg/1.9.5/org/mockito/…
Лэнс Кинд
18

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

Документы значения 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 для этого класса специально говорит, что он предназначен для частичной насмешки ...
Майк Грызун
3
Кроме того ... не столкнется ли это с проблемой, с которой сталкиваются другие ответы здесь: т.е. thenReturnбудет ли фактически выполняться метод (который может вызвать проблемы, хотя и не в этом примере), и поэтому doReturnпредпочтительнее в таком случае ...?
Майк Грызун
4

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

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

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

kflGalore
источник
2
Я чувствую, что этот ответ частично является мнением. Пожалуйста, рассмотрите возможность редактирования.
soundslikeodd
2
Проголосовал за то, чтобы подбодрить нового члена семьи. Не нужно вводить эту зону, нет ничего технически неправильного или неправильный язык / тон. Будьте добры к новым членам. Спасибо.
Саураб Патил