Шпион (Spy) в Mockito

JavaBeginner

Введение

Mockito - это мощный фреймворк для Java, используемый для мокирования (mocking) и модульного тестирования (unit testing) Java-приложений. В этом практическом занятии (LabEx) мы рассмотрим концепцию шпионов (spies) в Mockito. Шпион (Spy) представляет собой уникальный инструмент тестирования, который действует как частичный мок - он отслеживает взаимодействие с объектом, при этом позволяя вызывать реальные методы.

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

Создание проекта и понимание работы шпиона (Spy) в Mockito

На этом этапе мы настроим структуру нашего проекта и узнаем, что такое шпион (Spy) в Mockito. Шпионы позволяют отслеживать вызовы методов на реальных объектах, при этом используя их фактическую реализацию.

Создание Java-файла

Сначала создадим основной Java-файл для этого практического занятия (LabEx). В WebIDE создайте новый файл с именем MockitoSpyDemo.java в директории ~/project.

Создание нового файла

Добавьте следующий код для импорта необходимых зависимостей:

import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.MockitoAnnotations;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;

public class MockitoSpyDemo {

    public static void main(String[] args) {
        System.out.println("This is a Mockito Spy demonstration.");
        System.out.println("To run the tests, you need to use JUnit.");
    }
}

Понимание концепции шпиона (Spy) в Mockito

Шпион (Spy) в Mockito представляет собой обертку вокруг реального объекта, которая позволяет:

  1. Отслеживать все взаимодействия с объектом (как мок)
  2. Выполнять реальные методы объекта (в отличие от мока)

Представьте шпиона как наблюдателя, который следит за тем, что происходит с объектом, но не вмешивается в его нормальное поведение, если не программирован на это специально.

Добавим наш первый тестовый метод, чтобы продемонстрировать базовое использование шпиона. Добавьте следующий метод в класс MockitoSpyDemo:

@Test
public void basicSpyExample() {
    // Create a list and spy on it
    List<String> realList = new ArrayList<>();
    List<String> spyList = Mockito.spy(realList);

    // Using the spy to add an item (the real method is called)
    spyList.add("Hello");

    // Verify the interaction happened
    Mockito.verify(spyList).add("Hello");

    // The real method was called, so the list actually has the item
    assertEquals(1, spyList.size());
    assertEquals("Hello", spyList.get(0));

    System.out.println("Basic Spy Example - Spy List Content: " + spyList);
    System.out.println("Basic Spy Example - Spy List Size: " + spyList.size());
}

Скомпилируем и запустим наш код, чтобы проверить, работает ли он:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java
java -cp .:lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo
Запуск кода

Вы должны увидеть следующий вывод:

This is a Mockito Spy demonstration.
To run the tests, you need to use JUnit.

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

На следующем этапе мы рассмотрим различные способы создания шпионов в Mockito.

Различные способы создания шпионов (Spies) в Mockito

На этом этапе мы рассмотрим различные подходы к созданию шпионов (Spies) в Mockito. Мы узнаем, как создавать шпионов с использованием метода Mockito.spy() и аннотации @Spy.

Использование метода Mockito.spy()

Самый прямой способ создать шпиона - использовать метод Mockito.spy(). Добавим тестовый метод, который демонстрирует этот подход:

@Test
public void testSpyWithMockitoSpyMethod() {
    // Create an ArrayList and a spy of it
    ArrayList<Integer> realList = new ArrayList<>();
    ArrayList<Integer> spyList = Mockito.spy(realList);

    // Adding elements to the spy (real methods are called)
    spyList.add(5);
    spyList.add(10);
    spyList.add(15);

    // Verifying interactions
    Mockito.verify(spyList).add(5);
    Mockito.verify(spyList).add(10);
    Mockito.verify(spyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, spyList.size());

    System.out.println("Spy Method Example - Spy List Content: " + spyList);
    System.out.println("Spy Method Example - Spy List Size: " + spyList.size());
}

Использование аннотации @Spy

Другой способ создать шпиона - использовать аннотацию @Spy. Этот подход особенно полезен, когда вы хотите автоматически инициализировать шпионов. Добавьте следующий код в класс MockitoSpyDemo:

// Declare a spy using annotation
@Spy
ArrayList<Integer> annotationSpyList = new ArrayList<>();

@Before
public void initSpies() {
    // Initialize mocks and spies with annotations
    MockitoAnnotations.initMocks(this);
}

@Test
public void testSpyWithAnnotation() {
    // Adding elements to the spy
    annotationSpyList.add(5);
    annotationSpyList.add(10);
    annotationSpyList.add(15);

    // Verifying interactions
    Mockito.verify(annotationSpyList).add(5);
    Mockito.verify(annotationSpyList).add(10);
    Mockito.verify(annotationSpyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, annotationSpyList.size());

    System.out.println("Annotation Spy Example - Spy List Content: " + annotationSpyList);
    System.out.println("Annotation Spy Example - Spy List Size: " + annotationSpyList.size());
}

Понимание различий между подходами

Оба подхода создают шпиона, но они имеют разные области применения:

  1. Mockito.spy() более гибок и может быть использован в любом месте ваших тестовых методов.
  2. Аннотация @Spy более удобна, когда у вас много шпионов и вы хотите инициализировать их все сразу.

Метод MockitoAnnotations.initMocks(this) отвечает за инициализацию всех полей с аннотациями. В реальных проектах вы можете использовать @RunWith(MockitoJUnitRunner.class), чтобы эта инициализация выполнялась автоматически.

Скомпилируем обновленный код:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

Код должен скомпилироваться без ошибок. Наши тестовые методы теперь готовы, но, как упоминалось ранее, нам понадобится запускатель JUnit для автоматического выполнения этих тестов.

На следующем этапе мы узнаем, как задавать поведение шпионов (stubbing spies), чтобы переопределить их поведение по умолчанию.

Заглушки для шпионов (Spies) и обработка исключений

На этом этапе мы узнаем, как задавать поведение шпиона (stubbing), чтобы переопределить его поведение по умолчанию, и научимся обрабатывать исключения, которые могут возникнуть при использовании Mockito.

Задача поведения шпиона (Stubbing a Spy)

По умолчанию шпионы используют методы реальных объектов, но иногда мы хотим изменить это поведение в целях тестирования. Это называется "задачей поведения" (stubbing) шпиона. Добавим тестовый метод, который демонстрирует, как задавать поведение шпиона:

@Test
public void testStubbingSpy() {
    // Create a spy of ArrayList
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add an element
    spyList.add(5);

    // Verify the element was added
    Mockito.verify(spyList).add(5);

    // By default, contains() should return true for element 5
    assertTrue(spyList.contains(5));
    System.out.println("Stubbing Example - Default behavior: spyList.contains(5) = " + spyList.contains(5));

    // Stub the contains() method to always return false for the value 5
    Mockito.doReturn(false).when(spyList).contains(5);

    // Now contains() should return false, even though the element is in the list
    assertFalse(spyList.contains(5));
    System.out.println("Stubbing Example - After stubbing: spyList.contains(5) = " + spyList.contains(5));

    // The element is still in the list (stubbing didn't change the list content)
    assertEquals(1, spyList.size());
    assertEquals(Integer.valueOf(5), spyList.get(0));
    System.out.println("Stubbing Example - Spy List Content: " + spyList);
}

Важное примечание о задаче поведения (Stubbing)

При задаче поведения методов шпиона рекомендуется использовать doReturn(), doThrow(), doAnswer() и т. д., вместо when(). Это связано с тем, что при использовании шпионов синтаксис when().thenReturn() может вызвать реальный метод при задаче поведения, что может привести к неожиданным побочным эффектам.

Например:

// This might call the real get() method, causing issues if index 10 doesn't exist
Mockito.when(spyList.get(10)).thenReturn(99); // May throw IndexOutOfBoundsException

// This is the correct way to stub a spy
Mockito.doReturn(99).when(spyList).get(10); // Safe, doesn't call the real method

Обработка исключения NotAMockException

Если вы попытаетесь использовать методы проверки (verification) Mockito на обычном объекте (не моке или шпионе), вы получите исключение NotAMockException. Добавим тестовый метод, чтобы продемонстрировать это исключение:

@Test
public void testNotAMockException() {
    try {
        // Create a regular ArrayList (not a mock or spy)
        ArrayList<Integer> regularList = new ArrayList<>();
        regularList.add(5);

        // Try to verify an interaction on a regular object
        Mockito.verify(regularList).add(5);

        fail("Expected NotAMockException was not thrown");
    } catch (NotAMockException e) {
        // Expected exception
        System.out.println("NotAMockException Example - Caught expected exception: " + e.getMessage());
        assertTrue(e.getMessage().contains("Argument passed to verify() is not a mock"));
    }
}

Скомпилируем обновленный код:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

Код должен скомпилироваться без ошибок. На этом этапе мы узнали, как задавать поведение методов шпиона, чтобы переопределить их поведение по умолчанию, и как обрабатывать исключение NotAMockException, которое возникает при использовании методов проверки на обычных объектах.

На следующем этапе мы сравним моки и шпионы, чтобы понять их основные различия.

Mock vs Spy: Понимание различий

На этом этапе мы напрямую сравним моки (mocks) и шпионов (spies), чтобы понять их основные различия. Это сравнение поможет вам выбрать подходящий инструмент для ваших тестовых потребностей.

Создание теста для сравнения

Добавим тестовый метод, который демонстрирует различия между моками и шпионами:

@Test
public void testMockVsSpyDifference() {
    // Create a mock and a spy of ArrayList
    ArrayList<Integer> mockList = Mockito.mock(ArrayList.class);
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add elements to both
    mockList.add(1);
    spyList.add(1);

    // Verify interactions (both will pass)
    Mockito.verify(mockList).add(1);
    Mockito.verify(spyList).add(1);

    // Check the size of both lists
    System.out.println("Mock vs Spy - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy - Spy List Size: " + spyList.size());

    // Mock returns default values (0 for size) unless stubbed
    assertEquals(0, mockList.size());

    // Spy uses real method implementation, so size is 1
    assertEquals(1, spyList.size());

    // Stub both to return size 100
    Mockito.when(mockList.size()).thenReturn(100);
    Mockito.when(spyList.size()).thenReturn(100);

    // Both should now return 100 for size
    assertEquals(100, mockList.size());
    assertEquals(100, spyList.size());

    System.out.println("Mock vs Spy (After Stubbing) - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy (After Stubbing) - Spy List Size: " + spyList.size());
}

Понимание основных различий

Сводим основные различия между моками и шпионами в Mockito:

  1. Поведение методов:

    • Мок (Mock): Все методы возвращают значения по умолчанию (null, 0, false), если не задать другое поведение (stubbing).
    • Шпион (Spy): Методы вызывают реальную реализацию, если не задать другое поведение (stubbing).
  2. Состояние объекта:

    • Мок (Mock): Операции не изменяют состояние объекта (например, добавление элемента в мок-список не добавляет его на самом деле).
    • Шпион (Spy): Операции изменяют состояние объекта, как и в случае с реальным объектом.
  3. Создание:

    • Мок (Mock): Создается на основе класса, а не экземпляра (Mockito.mock(ArrayList.class)).
    • Шпион (Spy): Обычно создается на основе экземпляра (Mockito.spy(new ArrayList<>())).
  4. Области применения:

    • Мок (Mock): Когда вы хотите полностью контролировать поведение и не нужны реальные методы.
    • Шпион (Spy): Когда вы хотите использовать реальные методы, но нужно переопределить только некоторые аспекты поведения.

Практический пример: Когда использовать шпиона

Добавим еще один тест, который показывает распространенный случай использования шпионов - частичное задание поведения сложного объекта:

@Test
public void testWhenToUseSpy() {
    // Create a complex data object that we want to partially mock
    StringBuilder builder = new StringBuilder();
    StringBuilder spyBuilder = Mockito.spy(builder);

    // Use the real append method
    spyBuilder.append("Hello");
    spyBuilder.append(" World");

    // Verify the real methods were called
    Mockito.verify(spyBuilder).append("Hello");
    Mockito.verify(spyBuilder).append(" World");

    // The real methods modified the state
    assertEquals("Hello World", spyBuilder.toString());
    System.out.println("When to Use Spy - Content: " + spyBuilder.toString());

    // Stub the toString() method to return something else
    Mockito.when(spyBuilder.toString()).thenReturn("Stubbed String");

    // Now toString() returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
    System.out.println("When to Use Spy - After Stubbing toString(): " + spyBuilder.toString());

    // But other methods still work normally
    spyBuilder.append("!");
    Mockito.verify(spyBuilder).append("!");

    // toString() still returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
}

Скомпилируем обновленный код:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

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

Резюме

В этом практическом занятии (lab) мы изучили концепцию шпионов (spies) в Mockito, мощной технологии для юнит-тестирования на Java. Вот основные аспекты, которые мы рассмотрели:

  1. Создание шпионов (Spies): Мы узнали, как создавать шпионов с помощью метода Mockito.spy() и аннотации @Spy. Первый способ более гибок, а второй - более удобен, когда у вас много шпионов.

  2. Поведение шпионов (Spies): Шпионы отслеживают вызовы методов, как и моки (mocks), но по умолчанию выполняют реальные методы. Эта двойственная природа делает их особенно полезными для тестирования реальных объектов, сохраняя при этом возможность проверки взаимодействий.

  3. Задача поведения шпионов (Stubbing Spies): Мы научились переопределять поведение методов шпионов с помощью методов doReturn(), doThrow() и других методов задачи поведения (stubbing). Это позволяет нам контролировать определенные аспекты поведения, сохраняя при этом остальные части объекта нетронутыми.

  4. Обработка исключений: Мы изучили исключение NotAMockException, которое возникает при попытке использовать методы проверки Mockito на обычных объектах, и научились правильно его обрабатывать.

  5. Моки (Mocks) vs Шпионы (Spies): Мы сравнили моки и шпионов, чтобы понять их основные различия. Моки полностью заменяют исходное поведение на значения по умолчанию или заданные поведения (stubbed responses), в то время как шпионы сохраняют исходное поведение, но позволяют переопределять конкретные методы.

Шпионы (Spies) - мощный инструмент в арсенале Mockito, обеспечивающий баланс между полным контролем, предоставляемым моками, и реалистичностью реальных объектов. Они особенно полезны, когда:

  • Вы хотите протестировать реальный объект, но нужна проверка определенных взаимодействий.
  • Вам нужно переопределить только конкретные методы, сохраняя при этом остальное поведение объекта нетронутым.
  • Вы работаете с сложными объектами, при этом мокирование всех методов было бы нецелесообразно.

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