← Назад к вопросам
Как подменить один класс другим
2.0 Middle🔥 131 комментариев
#SOLID и паттерны проектирования#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как подменить один класс другим в Java
Подмена (substitution) класса — это замена реального объекта на его аналог. Это необходимо для тестирования, а также для изменения поведения без изменения кода.
1. Dependency Injection (Рекомендуется)
Самый чистый способ — передавать зависимости через конструктор или setter.
Constructor Injection
// Интерфейс для абстракции
public interface PaymentGateway {
void charge(double amount);
}
// Реальная реализация
public class StripeGateway implements PaymentGateway {
@Override
public void charge(double amount) {
// Реальный запрос к Stripe API
System.out.println("Charging " + amount + " via Stripe");
}
}
// Mock реализация для тестов
public class MockPaymentGateway implements PaymentGateway {
@Override
public void charge(double amount) {
System.out.println("Mock charge: " + amount);
}
}
// Класс, который использует зависимость
public class OrderService {
private PaymentGateway paymentGateway;
// Зависимость передаётся через конструктор
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void processOrder(Order order) {
paymentGateway.charge(order.getAmount());
System.out.println("Order processed");
}
}
// Использование в production
PaymentGateway stripe = new StripeGateway();
OrderService service = new OrderService(stripe);
// Использование в тестах
@Test
void testOrderProcessing() {
PaymentGateway mock = new MockPaymentGateway();
OrderService service = new OrderService(mock);
service.processOrder(new Order(100.0));
}
2. Mockito (Для юнит-тестов)
Автоматическое создание mock объектов.
class OrderServiceTest {
@Mock
PaymentGateway mockGateway;
@InjectMocks
OrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testSuccessfulPayment() {
// Arrange
when(mockGateway.charge(100.0)).thenReturn(true);
Order order = new Order(100.0);
// Act
orderService.processOrder(order);
// Assert
verify(mockGateway).charge(100.0);
}
@Test
void testPaymentFailure() {
// Arrange
when(mockGateway.charge(100.0)).thenThrow(PaymentException.class);
// Act & Assert
assertThrows(PaymentException.class, () -> {
orderService.processOrder(new Order(100.0));
});
}
}
3. Spring Dependency Injection
// Интерфейс
public interface EmailService {
void sendEmail(String to, String subject, String body);
}
// Реальная реализация
@Component
public class GmailService implements EmailService {
@Override
public void sendEmail(String to, String subject, String body) {
System.out.println("Sending email via Gmail");
}
}
// Mock реализация
@Component
public class MockEmailService implements EmailService {
@Override
public void sendEmail(String to, String subject, String body) {
System.out.println("Mock email sent to " + to);
}
}
// Сервис, использующий зависимость
@Service
public class NotificationService {
private final EmailService emailService;
public NotificationService(EmailService emailService) {
this.emailService = emailService;
}
public void notifyUser(String email, String message) {
emailService.sendEmail(email, "Notification", message);
}
}
// Тест с Spring
@SpringBootTest
class NotificationServiceTest {
@MockBean
EmailService emailService;
@Autowired
NotificationService notificationService;
@Test
void testNotification() {
notificationService.notifyUser("user@example.com", "Hello");
verify(emailService).sendEmail(
"user@example.com",
"Notification",
"Hello"
);
}
}
4. Strategy Pattern
Явное переключение между реализациями.
public enum SortingAlgorithm {
BUBBLE(new BubbleSort()),
QUICK(new QuickSort()),
MERGE(new MergeSort());
private final Sorter sorter;
SortingAlgorithm(Sorter sorter) {
this.sorter = sorter;
}
public void sort(int[] arr) {
sorter.sort(arr);
}
}
public interface Sorter {
void sort(int[] arr);
}
public class BubbleSort implements Sorter {
@Override
public void sort(int[] arr) {
System.out.println("Sorting with Bubble Sort");
// реализация
}
}
public class QuickSort implements Sorter {
@Override
public void sort(int[] arr) {
System.out.println("Sorting with Quick Sort");
// реализация
}
}
// Использование
SortingAlgorithm.QUICK.sort(arr);
SortingAlgorithm.MERGE.sort(arr);
5. Factory Pattern
Фабрика выбирает нужную реализацию.
public class DatabaseFactory {
public static Database create(String type) {
switch (type) {
case "mysql":
return new MysqlDatabase();
case "postgres":
return new PostgresDatabase();
case "mock":
return new MockDatabase();
default:
throw new IllegalArgumentException("Unknown DB type");
}
}
}
public interface Database {
void connect();
void query(String sql);
}
public class MysqlDatabase implements Database {
@Override
public void connect() {
System.out.println("Connecting to MySQL");
}
@Override
public void query(String sql) {
System.out.println("Executing MySQL query: " + sql);
}
}
public class MockDatabase implements Database {
@Override
public void connect() {
System.out.println("Mock connection");
}
@Override
public void query(String sql) {
System.out.println("Mock query result");
}
}
// Использование
Database db = DatabaseFactory.create("mysql");
Database mockDb = DatabaseFactory.create("mock");
6. Test Containers (Для интеграционных тестов)
@Testcontainers
public class UserRepositoryTest {
@Container
static PostgreSQLContainer postgres = new PostgreSQLContainer()
.withDatabaseName("testdb")
.withUsername("user")
.withPassword("password");
private UserRepository userRepository;
@BeforeEach
void setUp() {
// Подменяем реальную БД на контейнер
DataSource dataSource = createDataSource(postgres);
userRepository = new UserRepository(dataSource);
}
@Test
void testSaveUser() {
User user = new User("John", "john@example.com");
userRepository.save(user);
User retrieved = userRepository.findById(user.getId());
assertEquals("John", retrieved.getName());
}
}
7. Reflection (Advanced)
Динамическое изменение поведения через рефлекшн (обычно в фреймворках).
public class ReflectionUtil {
public static void replaceInstance(Object target, String fieldName, Object replacement)
throws NoSuchFieldException, IllegalAccessException {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, replacement);
}
}
// Использование
@Test
void testWithReflection() throws Exception {
OrderService service = new OrderService(new StripeGateway());
// Заменяем реальный gateway на mock
PaymentGateway mockGateway = new MockPaymentGateway();
ReflectionUtil.replaceInstance(service, "paymentGateway", mockGateway);
service.processOrder(new Order(100.0));
}
Best Practices
- Используй интерфейсы — не зависи от конкретных классов
- Dependency Injection — передавай зависимости, не создавай их внутри
- Constructor injection — лучше чем setter injection
- Mockito — стандарт для подмены в юнит-тестах
- Spring @MockBean — для Spring integration тестов
- TestContainers — для реалистичных интеграционных тестов
- Избегай reflection — используй только если нет альтернативы
Сравнение подходов
| Способ | Сложность | Использование | Когда |
|---|---|---|---|
| DI (constructor) | ✓✓ | Основной | Всегда |
| Mockito | ✓✓ | Unit-тесты | Быстрые тесты |
| Spring MockBean | ✓✓✓ | Integration | Spring apps |
| TestContainers | ✓✓✓ | Integration | Реальная БД |
| Reflection | ✓ | Legacy | Редко |
Депенденси инжекшн через конструктор и Mockito — это стандарт индустрии для качественного тестирования и разработки.