Как вы работали с мокированием (mocking) в тестах?

«Как вы работали с мокированием (mocking) в тестах?» — вопрос из категории Фреймворки тестирования, который задают на 24% собеседований AQA / Automation. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В своей практике тестирования я активно использую мокирование для изоляции тестируемого модуля (Unit Under Test) от его внешних зависимостей. Это позволяет сосредоточиться на проверке логики самого модуля, делая тесты быстрыми, стабильными и не зависящими от внешних сервисов (БД, API, файловой системы).

Основные сценарии применения и примеры (на Java с Mockito):

  1. Замена внешних сервисов и клиентов: Чаще всего мокаю репозитории для работы с БД или HTTP-клиенты для вызовов внешних API.

    // Тестируемый сервис, зависящий от UserRepository
    @Service
    public class UserService {
        private final UserRepository userRepository;
        // ... конструктор
        public User getUserById(Long id) {
            return userRepository.findById(id)
                    .orElseThrow(() -> new UserNotFoundException(id));
        }
    }
    
    // Юнит-тест с моком репозитория
    @ExtendWith(MockitoExtension.class)
    class UserServiceTest {
        @Mock
        private UserRepository userRepositoryMock;
        @InjectMocks
        private UserService userService;
    
        @Test
        void getUserById_ShouldReturnUser_WhenUserExists() {
            // Arrange (Подготовка): задаем поведение мока
            Long userId = 1L;
            User expectedUser = new User(userId, "John Doe");
            when(userRepositoryMock.findById(userId)).thenReturn(Optional.of(expectedUser));
    
            // Act (Действие): вызываем тестируемый метод
            User actualUser = userService.getUserById(userId);
    
            // Assert (Проверка): проверяем результат и взаимодействие
            assertEquals(expectedUser, actualUser);
            verify(userRepositoryMock).findById(userId); // Проверяем, что метод был вызван ровно 1 раз
        }
    
        @Test
        void getUserById_ShouldThrow_WhenUserNotFound() {
            // Arrange
            Long userId = 999L;
            when(userRepositoryMock.findById(userId)).thenReturn(Optional.empty());
    
            // Act & Assert
            assertThrows(UserNotFoundException.class, () -> userService.getUserById(userId));
            verify(userRepositoryMock).findById(userId);
        }
    }
  2. Проверка взаимодействий (verification): Использую verify() для проверки, что тестируемый код корректно взаимодействует с зависимостями — вызывает методы с правильными аргументами и нужное количество раз.

    // Проверяем, что метод save был вызван с конкретным объектом
    verify(userRepositoryMock).save(argThat(user -> user.getName().equals("Alice")));
    // Проверяем, что метод не был вызван ни разу
    verify(userRepositoryMock, never()).delete(any());
    // Проверяем точное количество вызовов
    verify(emailServiceMock, times(2)).sendWelcomeEmail(anyString());
  3. Имитация сложного поведения и исключений: Моки могут выбрасывать исключения или возвращать разные значения при последовательных вызовах.

    // Первый вызов вернет одно значение, второй — другое
    when(mock.randomInt())
        .thenReturn(10)
        .thenReturn(20);
    // Имитация исключения
    when(mock.process()).thenThrow(new RuntimeException("Service unavailable"));
  4. Использование Spy для частичного мокирования: Spy полезен, когда нужно заменить поведение только некоторых методов реального объекта, оставив остальные оригинальными.

    @Spy
    private RealService realServiceSpy = new RealService(); // Создаем spy на реальном объекте
    
    @Test
    void testWithSpy() {
        // Подменяем только тяжелый метод, остальные работают как есть
        doReturn("Mocked result").when(realServiceSpy).expensiveNetworkCall();
        // ... вызов тестируемого кода, который использует realServiceSpy
    }

Мой подход к мокированию:

  • Изоляция: Мокаю всё, что выходит за рамки памяти процесса: БД, файлы, сетевые вызовы, сторонние SDK.
  • Простота: Поведение моков должно быть максимально простым и предсказуемым.
  • "Не перемокать": Не мокаю простые value-объекты (DTO, модели) и статические утилитные классы, не имеющие состояния. Также стараюсь покрывать интеграционными тестами критические пути взаимодействия с реальными зависимостями.
  • Читаемость: Использую @Mock и @InjectMocks аннотации (с JUnit 5 + Mockito) для чистой и понятной настройки тестов.