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

В чем разница между @Spy и @Stub?

1.3 Junior🔥 231 комментариев
#Тестирование

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

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

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

Разница между @Spy и @Stub

Это вопрос о мокировании в тестировании. @Spy и Stub (заглушка) - это разные подходы к созданию тестовых двойников объектов, каждый со своей целью и поведением.

Важное уточнение

В Mockito существует @Spy, но @Stub не является аннотацией Mockito. Stub - это общий паттерн тестирования. Рассмотрим различие между ними:

@Spy в Mockito

@Spy создаёт шпиона (spy) - частичный mock реального объекта. Шпион сохраняет реальное поведение объекта, но при этом позволяет отслеживать вызовы методов и переопределять их поведение.

@RunWith(MockitoRunner.class)
public class SpyExample {
    
    @Spy
    private UserRepository userRepository = new UserRepository();
    
    @Test
    public void testSpy() {
        // Шпион работает как реальный объект
        User user = new User(1, "Alice");
        userRepository.save(user);
        
        // Но мы можем отследить вызовы
        verify(userRepository).save(user);
        
        // Можем переопределить поведение конкретного метода
        when(userRepository.findById(1)).thenReturn(Optional.of(user));
        
        // Остальные методы работают как в реальном объекте
        userRepository.getAll();  // Использует реальную реализацию
    }
}

Stub (заглушка) - паттерн тестирования

Stub - это объект, который возвращает заранее определённые ответы на вызовы методов. Он не содержит логики, просто возвращает фиксированные значения.

public class StubUserRepository implements UserRepository {
    @Override
    public Optional<User> findById(int id) {
        // Всегда возвращает одного и того же пользователя
        return Optional.of(new User(1, "Alice"));
    }
    
    @Override
    public List<User> getAll() {
        // Всегда возвращает фиксированный список
        return Arrays.asList(
            new User(1, "Alice"),
            new User(2, "Bob")
        );
    }
    
    @Override
    public void save(User user) {
        // Ничего не делает, просто заглушка
    }
}

// Использование
@Test
public void testWithStub() {
    UserRepository stub = new StubUserRepository();
    UserService service = new UserService(stub);
    
    // stub всегда возвращает одного и того же пользователя
    Optional<User> user = stub.findById(999);  // Вернёт Alice с id=1
    assertEquals("Alice", user.get().getName());
}

Различия в таблице

Аспект@SpyStub
ОсноваРеальный объект + мокированиеПолностью заменяет реальный объект
Реальная логикаВыполняется по умолчаниюОтсутствует, только заглушки
Отслеживание вызововДа, можно использовать verify()Нет встроенной поддержки
Переопределение методовЧастичное, выбранные методыВсе методы - заглушки
СложностьПростая настройкаНужно создавать класс
ГибкостьВысокая, динамичное мокированиеНизкая, фиксированные ответы
ИспользованиеКогда нужна реальная логика + контрольКогда нужны фиксированные ответы

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

public interface EmailService {
    void sendEmail(String to, String subject);
    List<Email> getSentEmails();
}

public class RealEmailService implements EmailService {
    private List<Email> sentEmails = new ArrayList<>();
    
    @Override
    public void sendEmail(String to, String subject) {
        System.out.println("Отправляю письмо на " + to);
        sentEmails.add(new Email(to, subject));
    }
    
    @Override
    public List<Email> getSentEmails() {
        return sentEmails;
    }
}

@RunWith(MockitoRunner.class)
public class EmailServiceSpyTest {
    
    @Spy
    private EmailService emailService = new RealEmailService();
    
    @Test
    public void testSpyTrackingCalls() {
        // Шпион выполняет реальный метод
        emailService.sendEmail("alice@example.com", "Hello");
        emailService.sendEmail("bob@example.com", "Hi");
        
        // И отслеживает вызовы
        verify(emailService, times(2)).sendEmail(anyString(), anyString());
        
        // Реальная логика работает
        List<Email> sent = emailService.getSentEmails();
        assertEquals(2, sent.size());
    }
    
    @Test
    public void testSpyParialMocking() {
        // Для одного метода переопределяем поведение
        doThrow(new RuntimeException("Email service down"))
            .when(emailService)
            .sendEmail("attacker@evil.com", "spam");
        
        // Проверяем, что исключение выбросилось
        assertThrows(RuntimeException.class, 
            () -> emailService.sendEmail("attacker@evil.com", "spam"));
        
        // Остальные методы работают как обычно
        emailService.sendEmail("alice@example.com", "Hello");
        verify(emailService).sendEmail("alice@example.com", "Hello");
    }
}

Практический пример 2: Stub

public class UserRepositoryStub implements UserRepository {
    private static final User STUB_USER = new User(1, "Alice", "alice@example.com");
    
    @Override
    public Optional<User> findById(int id) {
        // Игнорируем id, всегда возвращаем одного и того же
        return Optional.of(STUB_USER);
    }
    
    @Override
    public List<User> getAll() {
        return Arrays.asList(
            STUB_USER,
            new User(2, "Bob", "bob@example.com")
        );
    }
    
    @Override
    public void save(User user) {
        // Stub не сохраняет ничего
    }
    
    @Override
    public void delete(int id) {
        // Stub не удаляет ничего
    }
}

// Использование stub
@Test
public void testWithStub() {
    UserRepository stub = new UserRepositoryStub();
    UserService service = new UserService(stub);
    
    // Stub возвращает фиксированные значения
    User user = service.getUserById(999);  // Вернёт Alice
    assertEquals("Alice", user.getName());
    
    // Сохранение не происходит (это stub)
    service.createUser(new User(3, "Charlie", "charlie@example.com"));
    
    // Но getAll всё ещё возвращает стабберованный список
    List<User> all = service.getAllUsers();
    assertEquals(2, all.size());  // Alice и Bob
}

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

✅ Нужна реальная логика объекта (например, БД в памяти) ✅ Требуется отслеживать вызовы методов (verify) ✅ Нужно переопределить поведение некоторых методов ✅ Тестируется взаимодействие между объектами ✅ Нужна гибкость в настройке

@RunWith(MockitoRunner.class)
public class UserServiceTest {
    
    @Spy
    private EmailService emailService = new RealEmailService();
    
    @Mock
    private UserRepository userRepository;
    
    private UserService userService;
    
    @Before
    public void setup() {
        userService = new UserService(userRepository, emailService);
    }
    
    @Test
    public void testRegistration() {
        // Частично мокируем: реальная emailService, но контролируем userRepository
        User newUser = new User(1, "Alice", "alice@example.com");
        when(userRepository.findById(1)).thenReturn(Optional.of(newUser));
        
        userService.registerUser(newUser);
        
        // Проверяем, что письмо было отправлено
        verify(emailService).sendEmail("alice@example.com", "Welcome");
        
        // Проверяем, что пользователь был сохранён
        verify(userRepository).save(newUser);
    }
}

Когда использовать Stub

✅ Нужны фиксированные тестовые данные ✅ Не требуется отслеживание вызовов ✅ Требуется простота и понятность тестов ✅ Stub используется в нескольких тестах ✅ Зависимость не критична для теста

public class OrderServiceTest {
    
    private UserRepositoryStub userRepository = new UserRepositoryStub();
    private OrderService orderService;
    
    @Before
    public void setup() {
        orderService = new OrderService(userRepository);
    }
    
    @Test
    public void testOrderCreation() {
        // Stub предоставляет фиксированного пользователя
        User user = userRepository.findById(1).orElseThrow();
        Order order = orderService.createOrder(user, "Book", 100);
        
        assertEquals("Alice", order.getUserName());
        assertEquals(100, order.getPrice());
    }
}

Гибридный подход: Mock vs Spy vs Stub

@RunWith(MockitoRunner.class)
public class IntegrationTest {
    
    @Mock
    private ExternalAPI externalAPI;  // Полностью мокируем
    
    @Spy
    private Logger logger = new RealLogger();  // Логируем, но отслеживаем
    
    private UserRepository userRepository = new UserRepositoryStub();  // Stub для простоты
    
    @Test
    public void testComplexScenario() {
        when(externalAPI.fetchData()).thenReturn("data");
        
        // logger выполняет реальное логирование
        // userRepository возвращает фиксированные данные
        // externalAPI контролируется нами
        
        verify(logger).log(anyString());
    }
}

Вывод

  • @Spy - это мощный инструмент Mockito для частичного мокирования с отслеживанием
  • Stub - это паттерн для создания простых заглушек с фиксированными ответами
  • @Spy используется для сложных сценариев с реальной логикой
  • Stub используется для простых случаев с предсказуемыми данными
  • Часто используются вместе в одном тесте для разных зависимостей
В чем разница между @Spy и @Stub? | PrepBro