- Home »

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 процессе и поддерживать высокое качество кода.
Помните: хорошие тесты — это инвестиция в будущее проекта. Потратьте время на правильную настройку один раз, и это окупится сэкономленными часами отладки в продакшне.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.