Кратко и по делу — что менять, примеры интерфейсов/классов и стратегия тестирования. 1) Проблема Исходный метод смешивает 4 ответственности: валидация, персистенция, отправка почты и логирование. Это мешает тестируемости и расширению. 2) Что сделать (основные шаги) - Вынести каждую ответственность в отдельный интерфейс. - Внедрить зависимости через конструктор (инверсия зависимостей). - Возвращать результат или бросать специализированные исключения вместо «молчаливого» void, чтобы упростить проверку в тестах. - Логика оркестрации должна оставаться в новом классе (OrderProcessor) и быть максимально простая. - Избежать статики и прямых вызовов внешних ресурсов в коде (использовать абстракции), для тестов — мокать/фейкать реализации. 3) Пример интерфейсов public interface OrderValidator { void validate(Order order) throws ValidationException; } public interface OrderRepository { Order save(Order order) throws RepositoryException; } public interface EmailService { boolean sendOrderConfirmation(Order order) throws EmailException; } public interface AuditLogger { void info(String msg); void warn(String msg); void error(String msg, Throwable t); } 4) Рефакторинг: оркестратор + результат public final class OrderProcessor { private final OrderValidator validator; private final OrderRepository repository; private final EmailService emailService; private final AuditLogger logger; public OrderProcessor(OrderValidator validator, OrderRepository repository, EmailService emailService, AuditLogger logger) { this.validator = validator; this.repository = repository; this.emailService = emailService; this.logger = logger; } public ProcessResult process(Order order) { try { validator.validate(order); } catch (ValidationException ve) { logger.warn("Validation failed: " + ve.getMessage()); return ProcessResult.failure(Stage.VALIDATION, ve.getMessage()); } Order saved; try { saved = repository.save(order); } catch (RepositoryException re) { logger.error("Save failed", re); return ProcessResult.failure(Stage.PERSISTENCE, re.getMessage()); } boolean emailSent; try { emailSent = emailService.sendOrderConfirmation(saved); if (!emailSent) { logger.warn("Email service returned false"); return ProcessResult.partialSuccess(saved, Stage.EMAIL); } } catch (EmailException ee) { logger.error("Email failed", ee); return ProcessResult.partialSuccess(saved, Stage.EMAIL, ee.getMessage()); } logger.info("Order processed: " + saved.getId()); return ProcessResult.success(saved); } } public final class ProcessResult { public enum Status { SUCCESS, PARTIAL_SUCCESS, FAILURE } public enum Stage { VALIDATION, PERSISTENCE, EMAIL } private final Status status; private final Stage failedStage; // nullable private final String message; // nullable private final Order order; // nullable // фабричные методы: success, partialSuccess, failure + геттеры } 5) Дополнительные улучшения (по необходимости) - Асинхронная отправка почты (через очередь) — если email не должен блокировать save. - Retry/идемпотентность для сетевых операций. - Транзакции: оборачивать save + другие критичные действия в транзакцию, если нужно атомарно. - Сделать Order immutable / DTO для передачи между слоями. 6) Тестовая стратегия Unit tests (быстро, изолировано) - Мокать все зависимости (Mockito / MockK / ручные фейки). - Тесты: - Успешный сценарий: validator.validate() не кидает, repository.save() возвращает объект, emailService.send... возвращает true; assert: результат SUCCESS, verify(repository).save(order), verify(emailService).send... - Валидция провалилась: validator бросает ValidationException; assert: ProcessResult.failure со Stage.VALIDATION; verify(repository, never()).save(...). - Сохранение упало: repository.save() бросает; assert: FAILURE/PERSISTENCE, verify(emailService, never()). - Email провалился (false или исключение): assert: PARTIAL_SUCCESS, сохранение произошло, emailService вызван. - Логирование можно проверить через мок AuditLogger (verify вызовов). Integration tests (медленнее) - Поднять реальную (или in-memory) БД (H2 / Testcontainers) и реальный/локальный SMTP (GreenMail) и проверить end-to-end: request -> запись в БД -> фактическая отправка письма. - Тесты транзакций, консистенции данных. Contract / Component tests - Проверять поведение concrete EmailService (например, retry) отдельно. - Проверять repository на реальные сценарии. End-to-end / Acceptance - Полная система с реальным внешним окружением (или staging): проверка бизнес-правил и уведомлений. 7) Что проверять в тестах (ключевые ассерты) - Правильные взаимодействия (verify вызовов). - Корректные возвращаемые статусы/сообщения в ProcessResult. - Что при ошибках ненужные побочные эффекты не происходят (напр., при провальной валидации — нет сохранения/письма). - Поведение при исключениях (правильная обработка/логирование, транзакционная консистентность). 8) Заключение (одно предложение) Разделив обязанности и введя интерфейсы + DI, вы получите легко тестируемый, расширяемый и надёжный код: unit-тесты мокают зависимости и проверяют оркестрацию, integration/acceptance тесты покрывают реальные реализации.
1) Проблема
Исходный метод смешивает 4 ответственности: валидация, персистенция, отправка почты и логирование. Это мешает тестируемости и расширению.
2) Что сделать (основные шаги)
- Вынести каждую ответственность в отдельный интерфейс.
- Внедрить зависимости через конструктор (инверсия зависимостей).
- Возвращать результат или бросать специализированные исключения вместо «молчаливого» void, чтобы упростить проверку в тестах.
- Логика оркестрации должна оставаться в новом классе (OrderProcessor) и быть максимально простая.
- Избежать статики и прямых вызовов внешних ресурсов в коде (использовать абстракции), для тестов — мокать/фейкать реализации.
3) Пример интерфейсов
public interface OrderValidator {
void validate(Order order) throws ValidationException;
}
public interface OrderRepository {
Order save(Order order) throws RepositoryException;
}
public interface EmailService {
boolean sendOrderConfirmation(Order order) throws EmailException;
}
public interface AuditLogger {
void info(String msg);
void warn(String msg);
void error(String msg, Throwable t);
}
4) Рефакторинг: оркестратор + результат
public final class OrderProcessor {
private final OrderValidator validator;
private final OrderRepository repository;
private final EmailService emailService;
private final AuditLogger logger;
public OrderProcessor(OrderValidator validator,
OrderRepository repository,
EmailService emailService,
AuditLogger logger) {
this.validator = validator;
this.repository = repository;
this.emailService = emailService;
this.logger = logger;
}
public ProcessResult process(Order order) {
try {
validator.validate(order);
} catch (ValidationException ve) {
logger.warn("Validation failed: " + ve.getMessage());
return ProcessResult.failure(Stage.VALIDATION, ve.getMessage());
}
Order saved;
try {
saved = repository.save(order);
} catch (RepositoryException re) {
logger.error("Save failed", re);
return ProcessResult.failure(Stage.PERSISTENCE, re.getMessage());
}
boolean emailSent;
try {
emailSent = emailService.sendOrderConfirmation(saved);
if (!emailSent) {
logger.warn("Email service returned false");
return ProcessResult.partialSuccess(saved, Stage.EMAIL);
}
} catch (EmailException ee) {
logger.error("Email failed", ee);
return ProcessResult.partialSuccess(saved, Stage.EMAIL, ee.getMessage());
}
logger.info("Order processed: " + saved.getId());
return ProcessResult.success(saved);
}
}
public final class ProcessResult {
public enum Status { SUCCESS, PARTIAL_SUCCESS, FAILURE }
public enum Stage { VALIDATION, PERSISTENCE, EMAIL }
private final Status status;
private final Stage failedStage; // nullable
private final String message; // nullable
private final Order order; // nullable
// фабричные методы: success, partialSuccess, failure + геттеры
}
5) Дополнительные улучшения (по необходимости)
- Асинхронная отправка почты (через очередь) — если email не должен блокировать save.
- Retry/идемпотентность для сетевых операций.
- Транзакции: оборачивать save + другие критичные действия в транзакцию, если нужно атомарно.
- Сделать Order immutable / DTO для передачи между слоями.
6) Тестовая стратегия
Unit tests (быстро, изолировано)
- Мокать все зависимости (Mockito / MockK / ручные фейки).
- Тесты:
- Успешный сценарий: validator.validate() не кидает, repository.save() возвращает объект, emailService.send... возвращает true; assert: результат SUCCESS, verify(repository).save(order), verify(emailService).send...
- Валидция провалилась: validator бросает ValidationException; assert: ProcessResult.failure со Stage.VALIDATION; verify(repository, never()).save(...).
- Сохранение упало: repository.save() бросает; assert: FAILURE/PERSISTENCE, verify(emailService, never()).
- Email провалился (false или исключение): assert: PARTIAL_SUCCESS, сохранение произошло, emailService вызван.
- Логирование можно проверить через мок AuditLogger (verify вызовов).
Integration tests (медленнее)
- Поднять реальную (или in-memory) БД (H2 / Testcontainers) и реальный/локальный SMTP (GreenMail) и проверить end-to-end: request -> запись в БД -> фактическая отправка письма.
- Тесты транзакций, консистенции данных.
Contract / Component tests
- Проверять поведение concrete EmailService (например, retry) отдельно.
- Проверять repository на реальные сценарии.
End-to-end / Acceptance
- Полная система с реальным внешним окружением (или staging): проверка бизнес-правил и уведомлений.
7) Что проверять в тестах (ключевые ассерты)
- Правильные взаимодействия (verify вызовов).
- Корректные возвращаемые статусы/сообщения в ProcessResult.
- Что при ошибках ненужные побочные эффекты не происходят (напр., при провальной валидации — нет сохранения/письма).
- Поведение при исключениях (правильная обработка/логирование, транзакционная консистентность).
8) Заключение (одно предложение)
Разделив обязанности и введя интерфейсы + DI, вы получите легко тестируемый, расширяемый и надёжный код: unit-тесты мокают зависимости и проверяют оркестрацию, integration/acceptance тесты покрывают реальные реализации.