Что надо сделать с зависимостью класса при тестировании Unit теста
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Что надо сделать с зависимостью класса при тестировании Unit теста
Краткий ответ
При написании unit теста необходимо изолировать класс, который мы тестируем (SUT - System Under Test), подменяя его зависимости на mock-и или stub-и. Это позволяет тестировать класс в изоляции, не полагаясь на реальные зависимости, которые могут быть медленными, недоступными или иметь побочные эффекты.
Основные подходы
1. Mock-и (Mockito, MockK)
Mock - это полная подмена зависимости с отслеживанием вызовов. Мы можем:
- Подменить поведение
- Проверить, были ли вызваны методы
- Проверить с какими параметрами вызваны
// Класс который тестируем
public class OrderService {
private UserRepository userRepository;
private PaymentService paymentService;
public OrderService(UserRepository userRepository,
PaymentService paymentService) {
this.userRepository = userRepository;
this.paymentService = paymentService;
}
public Order createOrder(Long userId, BigDecimal amount) {
User user = userRepository.findById(userId);
if (user == null) throw new UserNotFoundException();
paymentService.charge(user.getPaymentMethod(), amount);
return new Order(user, amount);
}
}
// Unit тест
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@Mock
private UserRepository userRepository; // Mock зависимость
@Mock
private PaymentService paymentService; // Mock зависимость
@InjectMocks
private OrderService orderService; // Класс под тестом
@Test
public void testCreateOrderSuccess() {
// Arrange: подменяем поведение mock-ов
User mockUser = new User(1L, "John");
when(userRepository.findById(1L)).thenReturn(mockUser);
// Act: вызываем метод
Order order = orderService.createOrder(1L, new BigDecimal("100"));
// Assert: проверяем результат
assertNotNull(order);
// Verify: проверяем, что методы были вызваны правильно
verify(userRepository, times(1)).findById(1L);
verify(paymentService, times(1)).charge("card123", new BigDecimal("100"));
}
@Test
public void testCreateOrderUserNotFound() {
// Arrange: mock возвращает null
when(userRepository.findById(999L)).thenReturn(null);
// Act & Assert
assertThrows(UserNotFoundException.class, () -> {
orderService.createOrder(999L, new BigDecimal("100"));
});
// Verify: платёж не был обработан
verify(paymentService, never()).charge(anyString(), any());
}
}
2. Stub-и (просто подменённые объекты)
Stub - это простая подмена без отслеживания вызовов. Просто возвращает нужные значения.
// Простой stub вместо Mockito
public class UserRepositoryStub implements UserRepository {
@Override
public User findById(Long id) {
if (id == 1L) {
return new User(1L, "John");
}
return null;
}
}
// Использование в тесте
@Test
public void testOrderService() {
UserRepository stub = new UserRepositoryStub();
PaymentService paymentMock = mock(PaymentService.class);
OrderService service = new OrderService(stub, paymentMock);
Order order = service.createOrder(1L, new BigDecimal("100"));
assertNotNull(order);
}
3. Spy-и (частичный mock)
Spy - это реальный объект, но с возможностью подменять отдельные методы. Отслеживает вызовы реальных методов.
@Test
public void testWithSpy() {
UserRepository realRepository = new UserRepositoryImpl();
UserRepository spy = spy(realRepository); // Обёртка над реальным объектом
// Подменяем один метод
when(spy.findById(999L)).thenReturn(null);
// Остальные методы работают реально
List<User> users = spy.findAll(); // Реальный вызов БД
// Проверяем вызовы
verify(spy, times(1)).findById(999L);
}
4. Fake (упрощённая реализация)
Fake - это полная реализация интерфейса, но упрощённая для тестов. Например, in-memory БД вместо реальной.
// Fake PaymentService для тестов
public class FakePaymentService implements PaymentService {
private List<Payment> processedPayments = new ArrayList<>();
private boolean shouldFail = false;
@Override
public void charge(String method, BigDecimal amount) {
if (shouldFail) {
throw new PaymentException("Payment failed");
}
processedPayments.add(new Payment(method, amount));
}
public List<Payment> getProcessedPayments() {
return processedPayments;
}
public void setFailure(boolean fail) {
this.shouldFail = fail;
}
}
// Использование в тесте
@Test
public void testPaymentProcessing() {
FakePaymentService fakePayment = new FakePaymentService();
UserRepository stub = new UserRepositoryStub();
OrderService service = new OrderService(stub, fakePayment);
service.createOrder(1L, new BigDecimal("100"));
// Можем проверить что произошло внутри fake сервиса
assertEquals(1, fakePayment.getProcessedPayments().size());
}
Dependency Injection для тестирования
Чтобы легко подменять зависимости, нужно использовать Dependency Injection (DI):
❌ Плохо - зависимости hardcoded
public class OrderService {
private UserRepository userRepository = new UserRepositoryImpl(); // Hardcoded
public Order createOrder(Long userId, BigDecimal amount) {
User user = userRepository.findById(userId);
// ...
}
}
// Невозможно подменить для теста!
✓ Хорошо - зависимости через конструктор
public class OrderService {
private UserRepository userRepository;
public OrderService(UserRepository userRepository) { // DI
this.userRepository = userRepository;
}
public Order createOrder(Long userId, BigDecimal amount) {
User user = userRepository.findById(userId);
}
}
// Легко подменять в тестах
@Test
public void test() {
UserRepository mock = mock(UserRepository.class);
OrderService service = new OrderService(mock);
}
Пример полного Unit теста
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
void testRegisterNewUser() {
// Arrange
User newUser = new User("john@example.com", "John");
when(userRepository.save(any())).thenReturn(newUser);
// Act
User result = userService.register(newUser);
// Assert
assertEquals("John", result.getName());
// Verify
verify(userRepository, times(1)).save(any(User.class));
verify(emailService, times(1)).sendWelcomeEmail("john@example.com");
}
@Test
void testRegisterDuplicateEmail() {
// Arrange
User existingUser = new User("john@example.com", "John");
when(userRepository.findByEmail("john@example.com"))
.thenReturn(Optional.of(existingUser));
// Act & Assert
assertThrows(DuplicateEmailException.class, () -> {
userService.register(existingUser);
});
// Verify email не был отправлен
verify(emailService, never()).sendWelcomeEmail(anyString());
}
@Test
void testEmailFailureShouldRollback() {
// Arrange
User newUser = new User("john@example.com", "John");
doThrow(new EmailException()).when(emailService)
.sendWelcomeEmail(anyString());
// Act & Assert
assertThrows(EmailException.class, () -> {
userService.register(newUser);
});
// Verify пользователь не был сохранён
verify(userRepository, never()).save(any());
}
}
Что использовать в каких случаях
| Подход | Когда использовать | Плюсы | Минусы |
|---|---|---|---|
| Mock (Mockito) | Для большинства зависимостей | Легко подменять, отслеживать | Может быть громоздким |
| Stub | Простые возвращаемые значения | Простой код | Не отслеживает вызовы |
| Spy | Тестирование части реального кода | Гибкость | Может быть сложным |
| Fake | Нужна реальная логика альтернативной реализации | Реалистичное поведение | Требует написания кода |
Лучшие практики
- Тестируй только один класс за раз (SUT)
- Подменяй все внешние зависимости - БД, API, файловая система
- Используй DI - конструкторы или поля для инъекции зависимостей
- Verify методы которые должны быть вызваны
- Arrange → Act → Assert - структурируй тест по этому принципу
- Избегай реальных зависимостей - не обращайся в реальную БД, API
- Используй @Mock и @InjectMocks для удобства с Mockito
- Тесты должны быть быстрыми - именно поэтому используем mock-и
Unit тест должен изолировать класс и проверять только его логику, не полагаясь на реальные сервисы.