Ответ
В своей практике тестирования я активно использую мокирование для изоляции тестируемого модуля (Unit Under Test) от его внешних зависимостей. Это позволяет сосредоточиться на проверке логики самого модуля, делая тесты быстрыми, стабильными и не зависящими от внешних сервисов (БД, API, файловой системы).
Основные сценарии применения и примеры (на Java с Mockito):
-
Замена внешних сервисов и клиентов: Чаще всего мокаю репозитории для работы с БД или 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); } } -
Проверка взаимодействий (verification): Использую
verify()для проверки, что тестируемый код корректно взаимодействует с зависимостями — вызывает методы с правильными аргументами и нужное количество раз.// Проверяем, что метод save был вызван с конкретным объектом verify(userRepositoryMock).save(argThat(user -> user.getName().equals("Alice"))); // Проверяем, что метод не был вызван ни разу verify(userRepositoryMock, never()).delete(any()); // Проверяем точное количество вызовов verify(emailServiceMock, times(2)).sendWelcomeEmail(anyString()); -
Имитация сложного поведения и исключений: Моки могут выбрасывать исключения или возвращать разные значения при последовательных вызовах.
// Первый вызов вернет одно значение, второй — другое when(mock.randomInt()) .thenReturn(10) .thenReturn(20); // Имитация исключения when(mock.process()).thenThrow(new RuntimeException("Service unavailable")); -
Использование 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) для чистой и понятной настройки тестов.