Я шпионю со своим маленьким Мокито…

Зачем шпионить за своими зависимостями?

Инфраструктура тестирования Mockito генерирует моки, заглушки и шпионы для ваших зависимостей. Большинство людей знакомы с макетами и заглушками. В этой статье мы поговорим о шпионах — перехватчиках, которые позволяют вам проверить, как ваши классы взаимодействуют с их настоящими зависимостями.

📝 Примечание.Это очень простой пример. Вики-страница Mockito под названием Использование шпионов (и подделок) содержит три гораздо более сложных примера. Если вы хотите, чтобы я рассмотрел более сложные примеры, оставьте мне примечание в комментариях. А пока позвольте мне показать вам тривиальный пример, потому что:

1. Вы все еще можете найти его полезным, и
2. Эй, не все должно быть сложно, верно?

Шпион — это класс, который находится между тестируемым классом и его реальными зависимыми объектами. Это само определение одного из основных законов информатики: каждая проблема в информатике решается путем добавления слоя косвенности — чего-то между мной и тем, куда я хочу попасть. Шпион сидит перед фактической, созданной зависимостью.

Шпионить за списком

Документация Mockito (встроенная в его Javadocs) показывает шпиона, примененного к java.util.List. Почему вы когда-либо хотите сделать это?

Рассмотрим класс с именем AddingMachine, который суммирует список целых чисел, поступающих извне:

import java.util.List;

public class AddingMachine {
    private final List<Integer> numbers;

    public AddingMachine(List<Integer> numbers) {
        this.numbers = numbers;
    }

    public int getTotalUsingLoop() {
        int total = 0;
        int count = numbers.size();
        for (int i = 0; i < count; i++) {
            total += numbers.get(i);
        }
        return total;
    }
}

Список целых чисел предоставляется через конструктор AddingMachine. Этот список является зависимостью в смысле тестирования. Мы хотим доказать, что метод getTotalUsingLoop работает, предоставив список в тесте и проверив, что возвращается правильная сумма. Метод вызывает методы size и count для зависимого списка, а затем складывает значения из списка способом, который был бы неуместен в Java 1.0.

Чтобы протестировать AddingMachine изолированно, мы могли бы предоставить фиктивный список, но здесь это не очень хорошая идея. Несмотря на то, что документация Mockito использует фиктивные списки в нескольких своих примерах (поскольку разработчики Java хорошо знакомы с методами из интерфейса List), в ней четко указано, что на практике никогда не следует фиктивно использовать списки. Вместо этого используйте реальный список.

Общая рекомендация от команды Mockito (как вы можете видеть на этой странице в вики Mockito):

Никогда не издевайтесь над классом, которым вы не владеете.

Это нормально. Мы предоставим реальный список для нашего теста:

@Test  // Not using Mockito at all
public void getTotalWithRealList() {
    List<Integer> realList = List.of(1, 2, 3);

    AddingMachine machine = new AddingMachine(realList);

    assertEquals(1 + 2 + 3, machine.getTotalUsingLoop());
}

Этот тест в порядке, и он проходит. Несмотря на то, что мы использовали действительно старую школу реализацию метода, по крайней мере, мы знаем, что он работает. (Мы суммировали значения, используя оригинальный цикл for и метод get для извлечения значений, а не for-each или stream.)

Где появляется шпион? Начнем с разницы между макетом и заглушкой:

  • Заглушка – это класс, заменяющий зависимость и предоставляющий известные выходные данные для вызовов методов с известными входными данными.
  • Макет делает то же самое, но также позволяет вам убедиться, что тестируемый класс вызывает методы в макете нужное количество раз с правильными аргументами в правильном порядке.

Прямо сейчас мы тоже этого не делаем — мы используем реальный, созданный список для нашей зависимости. Нам удалось проверить, что реализация getTotalUsingLoop работает, но мы не смогли проверить протокол: методы size и get вызываются в списке нужное количество раз с правильными аргументами, в правильном порядке. В Java нет встроенного способа отслеживать вызовы этих методов. Если бы мы заботились об этом, нам пришлось бы писать эту функциональность самим.

Однако шпион Mockito может сделать эту работу за нас. Вот тот же тест, переписанный для использования статического метода spy из класса Mockito для переноса списка.

@Test
public void spyOnList() {
    // Spy on a real list
    List<Integer> spyList = spy(List.of(1, 2, 3));

    AddingMachine machine = new AddingMachine(spyList);

    assertEquals(1 + 2 + 3, machine.getTotalUsingLoop());

    // Can verify a spy
    verify(spyList).size();
    verify(spyList, times(3)).get(anyInt());
}

Теперь мы можем использовать статический метод Mockito.verify для проверки протокола, как мы это обычно делаем с макетом. Мы проверяем, что метод size шпиона вызывался ровно один раз, а метод get вызывался три раза с целочисленными аргументами.

Это довольно удобно. Мы можем использовать любую реальную зависимость и просто обернуть ее в шпион, и шпион будет отслеживать вызовы методов для нас. Лучше всего то, что это тривиально легко сделать, и это не повлияло ни на что другое в нашем тесте.

Опасность: использование шпионов в качестве частичной имитации

Опасность использования шпионов возникает, когда вы пытаетесь использовать их как частичные имитации, то есть вы возлагаете надежды на некоторые, но не на все существующие методы. Мы могли бы, например, установить ожидания в нашем шпионе списка, чтобы указать размер, который не соответствует количеству элементов, фактически содержащихся в списке. Такой тест - очень плохая идея:

@Test
public void partialMockOfList() {
    // Spy on an empty list
    List<Integer> spyList = spy(List.of());

    // Stub the size() method
    when(spyList.size()).thenReturn(3);

    assertFalse(spyList.isEmpty()); // Uh oh. Is the list empty or not?
}

Давайте посмотрим на проблемы, которые создает такой тест. Метод isEmpty в Listреализациях, таких как ArrayList, реализуется путем проверки того, возвращает ли size ноль или нет. Итак, если мы установим size для шпиона, а затем вызовем isEmpty, тест вернет false. Но, как видите, когда мы создавали список, мы не добавляли никаких элементов, так что список на самом деле пуст и isEmpty действительно должен возвращать true. Мы назвали то, что мы считали несвязанным методом (isEmpty), и оно взаимодействовало с нашими ожиданиями таким образом, которого мы не ожидали.

⚠ Будьте осторожны с частичной имитацией. Они по-прежнему могут быть полезны (и, на самом деле, вышеупомянутая вики-страница об их использовании содержит несколько полезных примеров), но убедитесь, что они не взаимодействуют с другими незаглушенными методами в вашем шпионе неожиданным образом.

Заключение

Шпионы позволяют вам перехватывать вызовы вашего класса для проверки его фактических зависимостей. Вы можете проверить, что методы в шпионе вызывались нужное количество раз, с правильными аргументами, в правильном порядке.

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

Весь код в этой статье содержится в репозитории GitHub для книги Mockito Made Clear.

Возьмите книгу:



Следите за автором: