Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Глубокая работа с моками и тестированием
Этот вопрос оценивает опыт в unit-тестировании и понимание различных уровней тестирования. Правильный ответ покажет, что разработчик знает когда, как и почему использовать моки.
Основы моков и их различие
// Три разных типа тестовых двойников:
// 1. Stub — объект, который возвращает предопределённые значения
public class PaymentGatewayStub implements PaymentGateway {
@Override
public PaymentResult charge(Payment payment) {
return new PaymentResult(true, "stub-transaction-id");
}
}
// 2. Mock — объект, который проверяет взаимодействия
PaymentGateway mockGateway = mock(PaymentGateway.class);
when(mockGateway.charge(any())).thenReturn(
new PaymentResult(true, "mock-id")
);
verify(mockGateway).charge(payment); // Проверка вызова
// 3. Spy — частично настоящий объект, с возможностью мониторинга
OrderRepository realRepo = new OrderRepository(db);
OrderRepository spyRepo = spy(realRepo);
when(spyRepo.findById(1)).thenReturn(Optional.of(order));
verify(spyRepo).findById(1);
Практический опыт: Mockito Framework
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class OrderServiceTest {
private OrderService orderService;
private PaymentGateway paymentGateway;
private OrderRepository orderRepository;
@BeforeEach
void setUp() {
// Создание моков
paymentGateway = mock(PaymentGateway.class);
orderRepository = mock(OrderRepository.class);
// Инъекция зависимостей
orderService = new OrderService(paymentGateway, orderRepository);
}
// Тест 1: Успешный платёж
@Test
void testSuccessfulPayment() {
Order order = new Order("ORD-001", BigDecimal.TEN);
// Настройка мока
when(paymentGateway.charge(any()))
.thenReturn(new PaymentResult(true, "txn-123"));
when(orderRepository.save(any()))
.thenReturn(order);
// Выполнение
OrderResult result = orderService.processOrder(order);
// Проверки
assertTrue(result.isSuccessful());
verify(paymentGateway).charge(any());
verify(orderRepository).save(any());
}
// Тест 2: Ошибка платежа
@Test
void testPaymentFailure() {
Order order = new Order("ORD-002", BigDecimal.TEN);
// Мок возвращает ошибку
when(paymentGateway.charge(any()))
.thenReturn(new PaymentResult(false, "Insufficient funds"));
// Должно выбросить исключение
assertThrows(PaymentException.class, () -> {
orderService.processOrder(order);
});
// Проверяем, что сохранение НЕ произошло
verify(orderRepository, never()).save(any());
}
// Тест 3: Проверка параметров вызова
@Test
void testPaymentCalledWithCorrectAmount() {
Order order = new Order("ORD-003", new BigDecimal("99.99"));
when(paymentGateway.charge(any()))
.thenReturn(new PaymentResult(true, "txn-456"));
orderService.processOrder(order);
// Проверка с argumentCaptor
ArgumentCaptor<Payment> captor = ArgumentCaptor.forClass(Payment.class);
verify(paymentGateway).charge(captor.capture());
Payment capturedPayment = captor.getValue();
assertEquals(new BigDecimal("99.99"), capturedPayment.getAmount());
}
// Тест 4: Проверка количества вызовов
@Test
void testPaymentGatewayCalledOnce() {
Order order = new Order("ORD-004", BigDecimal.TEN);
when(paymentGateway.charge(any()))
.thenReturn(new PaymentResult(true, "txn-789"));
orderService.processOrder(order);
// Проверяем ровно один вызов
verify(paymentGateway, times(1)).charge(any());
}
// Тест 5: Mock Exception
@Test
void testPaymentGatewayException() {
Order order = new Order("ORD-005", BigDecimal.TEN);
// Мок выбрасывает исключение
when(paymentGateway.charge(any()))
.thenThrow(new PaymentGatewayException("Service unavailable"));
assertThrows(PaymentGatewayException.class, () -> {
orderService.processOrder(order);
});
}
}
Продвинутые техники моков
Stubbing с Behavior
public class AdvancedMockingTest {
@Test
void testStubbingWithBehavior() {
List<String> mockList = mock(List.class);
// Разные результаты для разных вызовов
when(mockList.get(0)).thenReturn("first");
when(mockList.get(1)).thenReturn("second");
assertEquals("first", mockList.get(0));
assertEquals("second", mockList.get(1));
}
@Test
void testStubbingWithMatcher() {
UserRepository userRepo = mock(UserRepository.class);
// Любой ID вернёт пользователя
when(userRepo.findById(anyLong()))
.thenReturn(Optional.of(new User("John")));
// Вернёт другого для конкретного ID
when(userRepo.findById(eq(999L)))
.thenReturn(Optional.empty());
}
@Test
void testChainedMocks() {
Config config = mock(Config.class);
Database db = mock(Database.class);
// Цепочка вызовов
when(config.getDatabase()).thenReturn(db);
when(db.getConnection()).thenReturn(mock(Connection.class));
}
}
ArgumentCaptor для сложных проверок
public class ArgumentCaptorTest {
@Test
void captureAndVerifyComplexObject() {
OrderService service = new OrderService(mock(PaymentGateway.class));
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
ArgumentCaptor<Payment> paymentCaptor = ArgumentCaptor.forClass(Payment.class);
// Выполнить операцию
Order order = new Order("ORD-001", BigDecimal.TEN);
service.processOrder(order);
// Захватить и проверить все параметры
verify(service).save(orderCaptor.capture());
Order capturedOrder = orderCaptor.getValue();
assertTrue(capturedOrder.isProcessed());
}
}
Когда НЕ использовать моки
public class AvoidMockingTest {
// ❌ ПЛОХО: Мокирование всего подряд
@Test
void badTest() {
UserService mockService = mock(UserService.class);
OrderRepository mockRepo = mock(OrderRepository.class);
PaymentGateway mockGateway = mock(PaymentGateway.class);
NotificationService mockNotification = mock(NotificationService.class);
// Что я вообще тестирую?
}
// ✅ ХОРОШО: Мокирование только внешних зависимостей
@Test
void goodTest() {
// Реальные классы для логики
OrderService orderService = new OrderService(...);
User user = new User("John", "Doe");
// Мокирование только внешних сервисов
PaymentGateway paymentGateway = mock(PaymentGateway.class);
NotificationService notificationService = mock(NotificationService.class);
when(paymentGateway.charge(any()))
.thenReturn(new PaymentResult(true, "txn-123"));
OrderResult result = orderService.processOrder(user, new Order(...));
assertTrue(result.isSuccessful());
verify(notificationService).sendConfirmation(user);
}
}
Integration Testing vs Unit Testing
// UNIT TEST: Всё замокировано
@Test
public void unitTest_orderService() {
PaymentGateway paymentGateway = mock(PaymentGateway.class);
OrderRepository orderRepository = mock(OrderRepository.class);
// Быстро (< 100ms)
// Изолировано
// Но не проверяет интеграцию
}
// INTEGRATION TEST: Реальные компоненты
@SpringBootTest
public class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@MockBean
private PaymentGateway paymentGateway; // Мокируем только внешний API
@Test
void testOrderCreationAndPersistence() {
// Проверяет реальное создание и сохранение в БД
// Медленнее (может быть 1-2 сек)
// Но проверяет интеграцию компонентов
}
}
Практический опыт: Mock vs Real
public class DatabaseIntegrationExample {
// Сценарий 1: Юнит тест с mock
@Test
void unitTest_userValidation() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
when(mockRepo.findByEmail("user@example.com"))
.thenReturn(Optional.of(new User("John")));
assertTrue(service.userExists("user@example.com"));
// Проверяет только логику UserService
// Быстро
}
// Сценарий 2: Интеграционный тест с реальной БД
@Test
@Transactional
void integrationTest_userPersistence() {
User user = new User("John", "john@example.com");
User saved = userRepository.save(user);
Optional<User> found = userRepository.findByEmail("john@example.com");
assertTrue(found.isPresent());
assertEquals(saved.getId(), found.get().getId());
// Проверяет реальное сохранение и чтение
// Медленнее
}
// Сценарий 3: Гибрид — микс реального и мока
@Test
void hybridTest_orderProcessing() {
// Реальный репозиторий
OrderRepository realRepo = new OrderRepository(dataSource);
// Мокированный внешний API
PaymentGateway mockGateway = mock(PaymentGateway.class);
OrderService service = new OrderService(realRepo, mockGateway);
when(mockGateway.charge(any()))
.thenReturn(new PaymentResult(true, "txn-123"));
Order order = service.processOrder(new Order());
// Проверяем реальное сохранение и корректность логики
Optional<Order> saved = realRepo.findById(order.getId());
assertTrue(saved.isPresent());
}
}
Best Practices
public class MockingBestPractices {
// 1. Используй @ExtendWith(MockitoExtension.class)
@ExtendWith(MockitoExtension.class)
public class ProperMockTest {
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
}
// 2. Один Assert за тест
@Test
void singleAssertion() {
when(mockRepo.findById(1)).thenReturn(Optional.of(user));
User result = userService.getUser(1);
assertEquals("John", result.getName()); // Один assert
}
// 3. Описательные имена
@Test
void givenValidOrder_whenProcessing_thenPaymentGatewayCalled() {
// Arrange
Order order = new Order("ORD-001", BigDecimal.TEN);
when(paymentGateway.charge(any())).thenReturn(success());
// Act
OrderResult result = orderService.processOrder(order);
// Assert
verify(paymentGateway).charge(any());
}
// 4. Тестирование исключений
@Test
void whenPaymentFails_thenThrowException() {
when(paymentGateway.charge(any()))
.thenThrow(new PaymentException("Declined"));
assertThrows(PaymentException.class,
() -> orderService.processOrder(order));
}
}
Заключение
Глубокая работа с моками означает:
- Понимание типов — Stub, Mock, Spy и их различие
- Практика с Mockito — when/then/verify/captor
- Баланс — знать когда мокировать, когда использовать реальные объекты
- Integration Testing — не полагаться только на unit тесты
- Best Practices — четкая структура, описательные имена, один assert
Опытный Java разработчик:
- Пишет 80% unit тестов с моками
- 15% интеграционных тестов с реальными компонентами
- 5% e2e тестов
- Понимает trade-offs каждого подхода
- Может быстро создавать надежные тесты
Это ключевая компетенция для высокого качества кода.