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

Как подменить один класс другим

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

  1. Используй интерфейсы — не зависи от конкретных классов
  2. Dependency Injection — передавай зависимости, не создавай их внутри
  3. Constructor injection — лучше чем setter injection
  4. Mockito — стандарт для подмены в юнит-тестах
  5. Spring @MockBean — для Spring integration тестов
  6. TestContainers — для реалистичных интеграционных тестов
  7. Избегай reflection — используй только если нет альтернативы

Сравнение подходов

СпособСложностьИспользованиеКогда
DI (constructor)✓✓ОсновнойВсегда
Mockito✓✓Unit-тестыБыстрые тесты
Spring MockBean✓✓✓IntegrationSpring apps
TestContainers✓✓✓IntegrationРеальная БД
ReflectionLegacyРедко

Депенденси инжекшн через конструктор и Mockito — это стандарт индустрии для качественного тестирования и разработки.