← Назад к вопросам

Что такое Spy?

1.0 Junior🔥 171 комментариев
#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Spy в тестировании (Mockito)

Spy — это специальный тип тестового двойника (test double) в фреймворке Mockito, который позволяет частично мокировать реальный объект. В отличие от Mock, который полностью заменяет объект, Spy вызывает реальные методы оригинального объекта, но при этом позволяет следить за его вызовами и переопределять поведение отдельных методов.

Основное отличие: Mock vs Spy

Mock — полностью заменяет объект:

public class UserService {
    private UserRepository repository;
    
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
    
    public User getUser(String id) {
        return repository.findById(id);
    }
}

@Test
public void testWithMock() {
    // Mock — полностью заменяет repository
    UserRepository mockRepository = mock(UserRepository.class);
    when(mockRepository.findById("1")).thenReturn(new User("1", "John"));
    
    UserService service = new UserService(mockRepository);
    User user = service.getUser("1");
    
    // Используется заданное поведение, реальный метод не вызывается
    assertThat(user.getName()).isEqualTo("John");
    verify(mockRepository).findById("1");
}

Spy — вызывает реальные методы, но позволяет их мониторить:

@Test
public void testWithSpy() {
    UserRepository realRepository = new UserRepository();
    // Spy оборачивает реальный объект
    UserRepository spyRepository = spy(realRepository);
    
    UserService service = new UserService(spyRepository);
    User user = service.getUser("1");
    
    // Используется реальный метод findById()
    assertThat(user).isNotNull();
    verify(spyRepository).findById("1");  // Проверяем что метод был вызван
}

Создание Spy

1. Spy существующего объекта

@Test
public void spyOnRealObject() {
    List<String> realList = new ArrayList<>();
    // Spy оборачивает реальный список
    List<String> spyList = spy(realList);
    
    // Используются реальные методы
    spyList.add("hello");
    spyList.add("world");
    
    assertThat(spyList).hasSize(2);
    // Можем проверить вызовы
    verify(spyList).add("hello");
    verify(spyList).add("world");
}

2. Spy класса с помощью конструктора

@Test
public void spyWithClassType() {
    // Mockito создает реальный объект и оборачивает его
    UserRepository spyRepository = spy(new UserRepository());
    
    // Реальный метод вызывается
    User user = spyRepository.findById("1");
    assertThat(user).isNotNull();
}

3. Spy с аннотацией @Spy

public class UserServiceTest {
    @Spy
    private UserRepository userRepository = new UserRepository();
    
    @InjectMocks
    private UserService userService;
    
    @Test
    public void testWithSpyAnnotation() {
        User user = userService.getUser("1");
        assertThat(user).isNotNull();
        
        // Проверяем что метод был вызван
        verify(userRepository).findById("1");
    }
}

Переопределение методов в Spy

Spy позволяет переопределить поведение отдельных методов, сохраняя реальное поведение остальных:

@Test
public void partialMockingWithSpy() {
    UserRepository spyRepository = spy(new UserRepository());
    
    // Переопределяем один метод
    when(spyRepository.findById("999")).thenReturn(null);
    
    UserService service = new UserService(spyRepository);
    
    // Для ID 1 используется реальный метод
    User user1 = service.getUser("1");
    assertThat(user1).isNotNull();
    
    // Для ID 999 используется заданное поведение
    User user999 = service.getUser("999");
    assertThat(user999).isNull();
}

Использование doReturn vs when

Для Spy лучше использовать doReturn() вместо when(), чтобы избежать вызова реального метода:

@Test
public void spyWithDoReturn() {
    UserRepository spyRepository = spy(new UserRepository());
    
    // НЕПРАВИЛЬНО (вызовет реальный findById)
    // when(spyRepository.findById("2")).thenReturn(null);
    
    // ПРАВИЛЬНО (не вызывает реальный метод)
    doReturn(null).when(spyRepository).findById("2");
    
    User user = spyRepository.findById("2");
    assertThat(user).isNull();
}

Практический пример с Spy

public class Logger {
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
    
    public void error(String message, Exception e) {
        System.out.println("[ERROR] " + message + ": " + e.getMessage());
    }
}

public class PaymentService {
    private Logger logger;
    private PaymentGateway gateway;
    
    public PaymentService(Logger logger, PaymentGateway gateway) {
        this.logger = logger;
        this.gateway = gateway;
    }
    
    public boolean processPayment(double amount) {
        logger.log("Processing payment of " + amount);
        try {
            gateway.charge(amount);
            logger.log("Payment successful");
            return true;
        } catch (Exception e) {
            logger.error("Payment failed", e);
            return false;
        }
    }
}

@Test
public void testPaymentServiceWithSpy() {
    // Spy logger для мониторинга логирования
    Logger spyLogger = spy(new Logger());
    
    // Mock gateway
    PaymentGateway mockGateway = mock(PaymentGateway.class);
    when(mockGateway.charge(100)).thenReturn(true);
    
    PaymentService service = new PaymentService(spyLogger, mockGateway);
    boolean result = service.processPayment(100);
    
    // Проверяем результат
    assertThat(result).isTrue();
    
    // Проверяем что logger был вызван нужное количество раз
    verify(spyLogger, times(2)).log(anyString());
    verify(spyLogger, never()).error(anyString(), any());
}

Проверка вызовов методов в Spy

@Test
public void verifySpyMethodCalls() {
    List<String> spyList = spy(new ArrayList<>());
    
    spyList.add("item1");
    spyList.add("item2");
    spyList.get(0);
    
    // Проверка что методы были вызваны
    verify(spyList, times(2)).add(anyString());
    verify(spyList).get(0);
    
    // Проверка что метод не был вызван
    verify(spyList, never()).remove(anyObject());
    
    // Проверка порядка вызовов
    InOrder inOrder = inOrder(spyList);
    inOrder.verify(spyList).add("item1");
    inOrder.verify(spyList).add("item2");
    inOrder.verify(spyList).get(0);
}

Когда использовать Spy vs Mock

Используй Mock когда:

  • Нужно полностью контролировать поведение объекта
  • Тестируешь взаимодействие (какие методы вызваны)
  • Зависимость сложная или дорогостоящая
  • Нет готовой реализации
// Mock используется для внешних сервисов
PaymentGateway mockGateway = mock(PaymentGateway.class);
when(mockGateway.charge(amount)).thenReturn(true);

Используй Spy когда:

  • Нужно большинство реального поведения
  • Нужно переопределить только отдельные методы
  • Тестируешь часть реального объекта
  • Нужно следить за вызовами реальных методов
// Spy используется для частичного мокирования
Logger spyLogger = spy(new Logger());
verify(spyLogger).log(anyString());

Частые ошибки со Spy

// ОШИБКА 1: Использование when вместо doReturn
List<String> spyList = spy(new ArrayList<>());
when(spyList.get(0)).thenReturn("item");  // Выбросит IndexOutOfBoundsException

// ПРАВИЛЬНО:
doReturn("item").when(spyList).get(0);

// ОШИБКА 2: Забывают что реальный метод вызывается
UserRepository spyRepo = spy(new UserRepository());
when(spyRepo.findById("999")).thenReturn(null);
// Реальный метод всё равно вызовется, потом переопределится

// ПРАВИЛЬНО:
UserRepository spyRepo = spy(new UserRepository());
doReturn(null).when(spyRepo).findById("999");

Вывод

Spy — это мощный инструмент в Mockito для:

  • Частичного мокирования объектов
  • Мониторинга вызовов реальных методов
  • Тестирования сложных взаимодействий
  • Смешивания реального и заданного поведения

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