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

Связано ли тестирование с SOLID

2.0 Middle🔥 171 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью#ORM и Hibernate

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

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

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

SOLID и тестирование: глубокая связь

Да, связь глубокая и взаимная. Хорошее тестирование требует соблюдения SOLID, а соблюдение SOLID делает код легче тестировать. Это не совпадение — это дизайн!

Почему они связаны?

SOLID принципы разработаны для:

  • Гибкости кода
  • Снижения связанности
  • Повышения переиспользуемости

Хорошее тестирование требует:

  • Гибкости кода (для mock'ирования)
  • Слабой связанности (для изоляции)
  • Фокусировки на одной ответственности

Это идеально совпадает!

S — Single Responsibility Principle

Что говорит: Класс должен иметь одну причину для изменения

Как это помогает тестированию:

// ❌ Плохо: множество ответственностей
@Service
public class OrderBadService {
    public void processOrder(OrderRequest request) {
        // 1. Валидация
        if (request.getAmount() <= 0) {
            throw new InvalidAmountException();
        }
        
        // 2. Сохранение в БД
        Order order = new Order(request);
        hibernateSession.save(order);
        hibernateSession.flush();
        
        // 3. Отправка уведомления
        String emailBody = "Your order: " + order.getId();
        smtpClient.send("orders@company.com", request.getEmail(), emailBody);
        
        // 4. Логирование
        log.info("Order processed: " + order.getId());
    }
}

// Тест будет кошмар:
// - Нужно мокировать Hibernate
// - Нужно мокировать SMTP
// - Нужно мокировать логгер
// - Тест будет хрупким и медленным
@Test
void shouldProcessOrder() {
    // Setup: 50 строк кода для инициализации мок-объектов
    // Утверждение: непонятно, что именно тестируем
    // Поддержка: изменение в любой части сломает тест
}

✅ Хорошо: разделение ответственности

// Валидация отделена
@Service
public class OrderValidator {
    public void validate(OrderRequest request) {
        if (request.getAmount() <= 0) {
            throw new InvalidAmountException();
        }
        if (request.getCustomerId() == null) {
            throw new MissingCustomerException();
        }
    }
}

// Сохранение отделено
@Service
public class OrderRepository {
    private final JpaOrderRepository jpaRepo;
    
    public Order save(OrderRequest request) {
        Order order = new Order(request);
        return jpaRepo.save(order);
    }
}

// Уведомление отделено
@Service
public class OrderNotificationService {
    private final EmailService emailService;
    
    public void notifyOrderCreated(Order order, String customerEmail) {
        String body = "Your order: " + order.getId();
        emailService.send("orders@company.com", customerEmail, body);
    }
}

// Оркестрация
@Service
public class OrderService {
    private final OrderValidator validator;
    private final OrderRepository repository;
    private final OrderNotificationService notificationService;
    
    public void processOrder(OrderRequest request) {
        validator.validate(request);
        Order order = repository.save(request);
        notificationService.notifyOrderCreated(order, request.getEmail());
    }
}

// Теперь каждый сервис легко тестировать отдельно
@Test
void shouldValidateAmount() {
    OrderValidator validator = new OrderValidator();
    
    assertThrows(InvalidAmountException.class, () -> {
        validator.validate(new OrderRequest(0));
    });
}

@Test
void shouldSaveOrder() {
    JpaOrderRepository jpaRepoMock = mock(JpaOrderRepository.class);
    OrderRepository repository = new OrderRepository(jpaRepoMock);
    
    OrderRequest request = new OrderRequest("cust-1", 100);
    repository.save(request);
    
    verify(jpaRepoMock).save(any(Order.class));
}

O — Open/Closed Principle

Что говорит: Открыто для расширения, закрыто для модификации

Как это помогает тестированию:

// ❌ Плохо: закрыто для расширения
public class PaymentProcessor {
    public void process(Payment payment) {
        if (payment.getType() == PaymentType.CREDIT_CARD) {
            // Процесс кредитной карты
            validateCard(payment.getCard());
            chargeCard(payment);
        } else if (payment.getType() == PaymentType.PAYPAL) {
            // Процесс PayPal
            authenticatePayPal(payment.getPaypalAccount());
            chargePayPal(payment);
        }
        // Каждый новый тип платежа требует изменения этого класса!
    }
}

// Тестирование:
// - При добавлении нового типа платежа тест ломается
// - Нельзя протестировать PayPal отдельно от CREDIT_CARD логики

✅ Хорошо: открыто для расширения

// Интерфейс — контракт
public interface PaymentStrategy {
    void process(Payment payment);
}

// Конкретные реализации
@Component
public class CreditCardPaymentStrategy implements PaymentStrategy {
    @Override
    public void process(Payment payment) {
        validateCard(payment.getCard());
        chargeCard(payment);
    }
}

@Component
public class PayPalPaymentStrategy implements PaymentStrategy {
    @Override
    public void process(Payment payment) {
        authenticatePayPal(payment.getPaypalAccount());
        chargePayPal(payment);
    }
}

// Контекст
@Service
public class PaymentProcessor {
    private final Map<PaymentType, PaymentStrategy> strategies;
    
    public void process(Payment payment) {
        PaymentStrategy strategy = strategies.get(payment.getType());
        strategy.process(payment);
    }
}

// Тестирование каждой стратегии независимо
@Test
void shouldProcessCreditCard() {
    PaymentStrategy strategy = new CreditCardPaymentStrategy(
        mock(CardValidator.class),
        mock(CardCharger.class)
    );
    
    Payment payment = new Payment(PaymentType.CREDIT_CARD, "card-123");
    strategy.process(payment);  // Тестируем ТОЛЬКО эту стратегию
}

@Test
void shouldProcessPayPal() {
    PaymentStrategy strategy = new PayPalPaymentStrategy(
        mock(PayPalAuthenticator.class),
        mock(PayPalCharger.class)
    );
    
    Payment payment = new Payment(PaymentType.PAYPAL, "paypal-account");
    strategy.process(payment);  // Независимый тест
}

L — Liskov Substitution Principle

Что говорит: Подтип может заменить супертип без нарушения контракта

Как это помогает тестированию:

// ❌ Плохо: подвластные объекты нарушают контракт
public interface NotificationService {
    void send(String message);
}

public class EmailNotificationService implements NotificationService {
    @Override
    public void send(String message) {
        // Отправляет email
    }
}

public class SlackNotificationBroken implements NotificationService {
    @Override
    public void send(String message) {
        // Выбрасывает exception если сообщение > 100 символов
        if (message.length() > 100) {
            throw new MessageTooLongException();
        }
        // Отправляет в Slack
    }
}

// Клиент ожидает, что может отправить любую длину
NotificationService service = new SlackNotificationBroken();
service.send("This is a very long message that will definitely...");  // BOOM!

✅ Хорошо: честное выполнение контракта

public interface NotificationService {
    void send(String message);  // Контракт: отправить любую строку
}

public class SlackNotificationProper implements NotificationService {
    @Override
    public void send(String message) {
        // Сжимаем длинные сообщения, но НЕ выбрасываем ошибку
        String truncated = message.length() > 100 ? 
            message.substring(0, 97) + "..." : 
            message;
        sendToSlack(truncated);
    }
}

// Теперь любая реализация может заменить другую
@Test
void shouldSendNotificationWithAnyImplementation() {
    List<NotificationService> services = List.of(
        new EmailNotificationService(),
        new SlackNotificationProper(),
        new SmsNotificationService()
    );
    
    // Все реализации работают одинаково
    for (NotificationService service : services) {
        service.send("Important message");
        // Ни одна не выбросит unexpected exception
    }
}

I — Interface Segregation Principle

Что говорит: Зависи от узких интерфейсов, не от толстых

Как это помогает тестированию:

// ❌ Плохо: толстый интерфейс
public interface UserService {
    User createUser(UserRequest request);
    User updateUser(String id, UserRequest request);
    void deleteUser(String id);
    User findById(String id);
    List<User> search(String query);
    void sendEmail(String userId, String email);
    void resetPassword(String userId);
    boolean validatePassword(String password);
    void loginUser(String userId);
    void logoutUser(String userId);
    // ... и ещё 20 методов
}

// При тестировании нужно мокировать ВСЕ методы
public class OrderServiceTest {
    @Test
    void shouldCreateOrder() {
        UserService userServiceMock = mock(UserService.class);
        // Нужно setup'ить 30 методов!
        when(userServiceMock.findById(anyString()))
            .thenReturn(new User("user-1"));
        // Всё остальное?
    }
}

✅ Хорошо: узкие интерфейсы

// Разделяем на мелкие интерфейсы
public interface UserProvider {
    User findById(String id);
}

public interface UserCreator {
    User create(UserRequest request);
}

public interface PasswordValidator {
    boolean validate(String password);
}

// Сервис зависит только от необходимого
public class OrderService {
    private final UserProvider userProvider;
    
    public void createOrder(OrderRequest request) {
        User user = userProvider.findById(request.getUserId());
        // Работаем с пользователем
    }
}

// Тестирование простое
@Test
void shouldCreateOrder() {
    UserProvider userProviderMock = mock(UserProvider.class);
    when(userProviderMock.findById("user-1"))
        .thenReturn(new User("user-1"));
    
    OrderService service = new OrderService(userProviderMock);
    Order order = service.createOrder(new OrderRequest("user-1", 100));
    
    assertNotNull(order);
}

D — Dependency Inversion Principle

Что говорит: Зависи от абстракций, не от конкретных реализаций

Как это помогает тестированию:

// ❌ Плохо: зависимость от конкретной реализации
public class OrderService {
    private final PostgresUserRepository userRepository;  // Конкретная реализация!
    private final RabbitMQEventPublisher eventPublisher;  // Конкретная реализация!
    
    public OrderService() {
        this.userRepository = new PostgresUserRepository();  // Hard dependency
        this.eventPublisher = new RabbitMQEventPublisher();  // Hard dependency
    }
}

// Тестирование невозможно без реальной БД и RabbitMQ!

✅ Хорошо: инверсия зависимостей

public interface UserRepository {
    Optional<User> findById(String id);
}

public interface EventPublisher {
    void publish(DomainEvent event);
}

// Зависимость от интерфейсов
public class OrderService {
    private final UserRepository userRepository;
    private final EventPublisher eventPublisher;
    
    public OrderService(
        UserRepository userRepository,
        EventPublisher eventPublisher
    ) {
        this.userRepository = userRepository;
        this.eventPublisher = eventPublisher;
    }
}

// Тестирование с mock'ами
@Test
void shouldPublishEventWhenOrderCreated() {
    UserRepository userRepoMock = mock(UserRepository.class);
    EventPublisher eventPublisherMock = mock(EventPublisher.class);
    OrderService service = new OrderService(userRepoMock, eventPublisherMock);
    
    when(userRepoMock.findById("user-1"))
        .thenReturn(Optional.of(new User("user-1")));
    
    Order order = service.createOrder("user-1", 100);
    
    verify(eventPublisherMock).publish(any(OrderCreatedEvent.class));
}

Вывод

SOLID и тестирование — это две стороны одной медали:

SOLID ПринципТестирование Улучшает
Single ResponsibilityФокусированные, быстрые тесты
Open/ClosedНезависимые тесты для каждого сценария
Liskov SubstitutionГарантия, что реализации взаимозаменяемы
Interface SegregationПростые mock'и, не перегруженные
Dependency InversionПолная изоляция через инъекцию

Практический совет: Если код сложно тестировать — это сигнал, что нарушены SOLID принципы. Тесты работают как детектор для плохого дизайна!