Связано ли тестирование с SOLID
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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 принципы. Тесты работают как детектор для плохого дизайна!