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

Насколько глубоко работал с моками

2.2 Middle🔥 181 комментариев
#Тестирование

Комментарии (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));
    }
}

Заключение

Глубокая работа с моками означает:

  1. Понимание типов — Stub, Mock, Spy и их различие
  2. Практика с Mockito — when/then/verify/captor
  3. Баланс — знать когда мокировать, когда использовать реальные объекты
  4. Integration Testing — не полагаться только на unit тесты
  5. Best Practices — четкая структура, описательные имена, один assert

Опытный Java разработчик:

  • Пишет 80% unit тестов с моками
  • 15% интеграционных тестов с реальными компонентами
  • 5% e2e тестов
  • Понимает trade-offs каждого подхода
  • Может быстро создавать надежные тесты

Это ключевая компетенция для высокого качества кода.

Насколько глубоко работал с моками | PrepBro