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

Что надо сделать с зависимостью класса при тестировании Unit теста

1.8 Middle🔥 241 комментариев
#SOLID и паттерны проектирования#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Что надо сделать с зависимостью класса при тестировании 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Нужна реальная логика альтернативной реализацииРеалистичное поведениеТребует написания кода

Лучшие практики

  1. Тестируй только один класс за раз (SUT)
  2. Подменяй все внешние зависимости - БД, API, файловая система
  3. Используй DI - конструкторы или поля для инъекции зависимостей
  4. Verify методы которые должны быть вызваны
  5. Arrange → Act → Assert - структурируй тест по этому принципу
  6. Избегай реальных зависимостей - не обращайся в реальную БД, API
  7. Используй @Mock и @InjectMocks для удобства с Mockito
  8. Тесты должны быть быстрыми - именно поэтому используем mock-и

Unit тест должен изолировать класс и проверять только его логику, не полагаясь на реальные сервисы.