Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Разница между @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());
}
Различия в таблице
| Аспект | @Spy | Stub |
|---|---|---|
| Основа | Реальный объект + мокирование | Полностью заменяет реальный объект |
| Реальная логика | Выполняется по умолчанию | Отсутствует, только заглушки |
| Отслеживание вызовов | Да, можно использовать 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 используется для простых случаев с предсказуемыми данными
- Часто используются вместе в одном тесте для разных зависимостей