Home » Mockito InjectMocks и Mocks — внедрение зависимостей в юнит-тестах
Mockito InjectMocks и Mocks — внедрение зависимостей в юнит-тестах

Mockito InjectMocks и Mocks — внедрение зависимостей в юнит-тестах

Когда-то запуск тестов на сервере был головной болью. Сейчас это обычная практика в CI/CD пайплайнах. Но есть одна штука, которая до сих пор вызывает вопросы даже у опытных разработчиков — правильное внедрение зависимостей в юнит-тестах. Mockito с аннотациями @InjectMocks и @Mock — это не просто удобство, а основа для создания быстрых и надёжных тестов. В этой статье разберём, как правильно настроить всё это дело на серверах, где запускается ваш CI/CD, и покажем практические примеры, которые сохранят вам нервы и время.

Как это работает под капотом

Mockito InjectMocks — это механизм внедрения моков в тестируемый класс. Когда вы помечаете поле аннотацией @InjectMocks, Mockito автоматически создаёт экземпляр этого класса и внедряет в него все зависимости, помеченные @Mock или @Spy.

Внедрение происходит в три этапа:

  • Constructor injection — сначала пытается найти подходящий конструктор
  • Property setter injection — затем ищет сеттеры для свойств
  • Field injection — в конце напрямую устанавливает значения через рефлексию

Пошаговая настройка и конфигурация

Для работы с Mockito на сервере нужно правильно настроить окружение. Вот базовая конфигурация:

// pom.xml
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>

Базовый тестовый класс выглядит так:

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    public void shouldCreateUser() {
        // given
        User user = new User("john@example.com");
        when(userRepository.save(any(User.class))).thenReturn(user);
        
        // when
        User result = userService.createUser("john@example.com");
        
        // then
        assertThat(result.getEmail()).isEqualTo("john@example.com");
        verify(emailService).sendWelcomeEmail(user);
    }
}

Практические примеры и кейсы

Рассмотрим несколько реальных сценариев, с которыми столкнётся каждый, кто настраивает тесты на серверах:

Кейс 1: Внедрение через конструктор

public class OrderService {
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    
    public OrderService(PaymentService paymentService, InventoryService inventoryService) {
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }
    
    public Order processOrder(OrderRequest request) {
        if (inventoryService.isAvailable(request.getProductId())) {
            return paymentService.processPayment(request);
        }
        throw new ProductNotAvailableException();
    }
}

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
    
    @Mock
    private PaymentService paymentService;
    
    @Mock
    private InventoryService inventoryService;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    public void shouldProcessOrderWhenProductAvailable() {
        // given
        OrderRequest request = new OrderRequest("product-1", 100);
        Order expectedOrder = new Order("order-1", "product-1", 100);
        
        when(inventoryService.isAvailable("product-1")).thenReturn(true);
        when(paymentService.processPayment(request)).thenReturn(expectedOrder);
        
        // when
        Order result = orderService.processOrder(request);
        
        // then
        assertThat(result.getProductId()).isEqualTo("product-1");
        verify(inventoryService).isAvailable("product-1");
        verify(paymentService).processPayment(request);
    }
}

Кейс 2: Смешанное внедрение (поля + конструктор)

public class ReportService {
    private final DatabaseService databaseService;
    
    @Autowired
    private CacheService cacheService;
    
    @Autowired
    private ConfigurationService configService;
    
    public ReportService(DatabaseService databaseService) {
        this.databaseService = databaseService;
    }
    
    public Report generateReport(String reportId) {
        if (cacheService.contains(reportId)) {
            return cacheService.get(reportId);
        }
        
        ReportConfig config = configService.getConfig();
        Report report = databaseService.fetchData(reportId, config);
        cacheService.put(reportId, report);
        
        return report;
    }
}

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

Тип внедрения Преимущества Недостатки Когда использовать
Constructor Injection Явные зависимости, immutable объекты Много параметров в конструкторе Обязательные зависимости
Field Injection Краткость кода, простота Скрытые зависимости, сложность тестирования Простые случаи, прототипы
Setter Injection Опциональные зависимости Возможность изменения после создания Конфигурируемые зависимости

Распространённые ошибки и их решения

Ошибка 1: NPE при внедрении

// Неправильно
@InjectMocks
private UserService userService = new UserService(); // НЕ делайте так!

// Правильно
@InjectMocks
private UserService userService;

Ошибка 2: Конфликт типов

// Проблема: два мока одного типа
@Mock
private NotificationService emailNotificationService;

@Mock
private NotificationService smsNotificationService;

@InjectMocks
private UserService userService; // Какой мок внедрить?

// Решение: использовать @Qualifier или разные типы
@Mock
@Qualifier("email")
private NotificationService emailNotificationService;

@Mock
@Qualifier("sms")
private NotificationService smsNotificationService;

Настройка для CI/CD на серверах

При запуске тестов на серверах важно правильно настроить окружение. Вот конфигурация для различных CI систем:

Jenkins Pipeline

pipeline {
    agent any
    
    tools {
        maven 'Maven-3.8.1'
        jdk 'JDK-17'
    }
    
    stages {
        stage('Test') {
            steps {
                sh 'mvn clean test -Dtest.profile=ci'
            }
        }
    }
    
    post {
        always {
            junit 'target/surefire-reports/*.xml'
            publishHTML([
                allowMissing: false,
                alwaysLinkToLastBuild: true,
                keepAll: true,
                reportDir: 'target/site/jacoco',
                reportFiles: 'index.html',
                reportName: 'Coverage Report'
            ])
        }
    }
}

GitHub Actions

name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    
    - name: Cache dependencies
      uses: actions/cache@v3
      with:
        path: ~/.m2
        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
    
    - name: Run tests
      run: mvn clean test --batch-mode
    
    - name: Generate test report
      uses: dorny/test-reporter@v1
      if: success() || failure()
      with:
        name: Maven Tests
        path: target/surefire-reports/*.xml
        reporter: java-junit

Альтернативные решения

Кроме Mockito, есть другие инструменты для мокирования:

  • EasyMock — более старый, но стабильный фреймворк
  • PowerMock — для мокирования статических методов и final классов
  • WireMock — для мокирования HTTP-сервисов
  • TestContainers — для интеграционных тестов с реальными базами данных

Сравнение производительности на сервере с 4GB RAM:

Фреймворк Время создания мока (мс) Потребление памяти (MB) Время выполнения 1000 тестов
Mockito 0.3 2.1 45s
EasyMock 0.5 2.8 52s
PowerMock 1.2 4.5 78s

Продвинутые техники и автоматизация

Кастомные моки для сложных случаев

@ExtendWith(MockitoExtension.class)
public class AdvancedServiceTest {
    
    @Mock
    private ExternalApiService externalApiService;
    
    @InjectMocks
    private DataProcessorService dataProcessorService;
    
    @Test
    public void shouldHandleApiTimeout() {
        // given
        when(externalApiService.fetchData(anyString()))
            .thenThrow(new TimeoutException("API timeout"));
        
        // when & then
        assertThatThrownBy(() -> dataProcessorService.processData("test"))
            .isInstanceOf(ServiceUnavailableException.class)
            .hasMessage("External service is temporarily unavailable");
        
        verify(externalApiService, timeout(1000)).fetchData("test");
    }
    
    @Test
    public void shouldRetryOnFailure() {
        // given
        when(externalApiService.fetchData(anyString()))
            .thenThrow(new ConnectException("Connection failed"))
            .thenThrow(new ConnectException("Connection failed"))
            .thenReturn(new ApiResponse("success"));
        
        // when
        ApiResponse response = dataProcessorService.processDataWithRetry("test");
        
        // then
        assertThat(response.getStatus()).isEqualTo("success");
        verify(externalApiService, times(3)).fetchData("test");
    }
}

Автоматизация с помощью TestContainers

@TestMethodOrder(OrderAnnotation.class)
@Testcontainers
public class IntegrationTest {
    
    @Container
    static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @Container
    static GenericContainer redis = new GenericContainer<>("redis:6-alpine")
            .withExposedPorts(6379);
    
    @Mock
    private ExternalPaymentService paymentService;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    @Order(1)
    public void shouldProcessOrderWithRealDatabase() {
        // given
        String jdbcUrl = postgres.getJdbcUrl();
        String redisUrl = redis.getHost() + ":" + redis.getMappedPort(6379);
        
        when(paymentService.processPayment(any())).thenReturn(PaymentResult.success());
        
        // when
        Order order = orderService.createOrder(new OrderRequest("product-1", 100));
        
        // then
        assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
    }
}

Мониторинг и отладка тестов на сервере

Для мониторинга выполнения тестов на продакшн-серверах используйте следующие настройки:

# application-test.properties
logging.level.org.mockito=DEBUG
logging.level.org.springframework.test=DEBUG

# Для отладки внедрения зависимостей
mockito.verbose=true
mockito.debug=true

# JVM параметры для CI
-Xmx2g -Xms1g -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap

Для VPS с ограниченными ресурсами рекомендуется настроить параллельное выполнение тестов:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M9</version>
    <configuration>
        <parallel>methods</parallel>
        <threadCount>4</threadCount>
        <perCoreThreadCount>true</perCoreThreadCount>
        <useUnlimitedThreads>false</useUnlimitedThreads>
    </configuration>
</plugin>

Интеграция с системами мониторинга

Для крупных проектов, развёрнутых на выделенных серверах, важно настроить мониторинг выполнения тестов:

// Кастомный TestExecutionListener для метрик
public class MetricsTestExecutionListener implements TestExecutionListener {
    
    private final MeterRegistry meterRegistry;
    
    @Override
    public void testSuccessful(TestIdentifier testIdentifier) {
        meterRegistry.counter("test.success", 
            "class", testIdentifier.getDisplayName()).increment();
    }
    
    @Override
    public void testFailed(TestIdentifier testIdentifier, Throwable throwable) {
        meterRegistry.counter("test.failure", 
            "class", testIdentifier.getDisplayName(),
            "error", throwable.getClass().getSimpleName()).increment();
    }
}

Заключение и рекомендации

Mockito InjectMocks — это мощный инструмент, который значительно упрощает создание юнит-тестов. Правильная настройка внедрения зависимостей критически важна для стабильной работы CI/CD пайплайнов на серверах.

Ключевые рекомендации:

  • Используйте constructor injection для обязательных зависимостей
  • Избегайте создания экземпляров в полях, помеченных @InjectMocks
  • Настройте правильные JVM параметры для CI-серверов
  • Мониторьте производительность тестов в продакшне
  • Используйте TestContainers для интеграционных тестов

Для небольших проектов достаточно базовой конфигурации Mockito, но для enterprise-решений рекомендую настроить полноценную систему мониторинга тестов с метриками и alerting. Это поможет быстро выявлять проблемы в CI/CD процессе и поддерживать высокое качество кода.

Помните: хорошие тесты — это инвестиция в будущее проекта. Потратьте время на правильную настройку один раз, и это окупится сэкономленными часами отладки в продакшне.


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked