Как писать модульные тесты в Java с JUnit и Mockito?

«Как писать модульные тесты в Java с JUnit и Mockito?» — вопрос из категории Тестирование, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Модульные тесты изолируют и проверяют отдельные компоненты (обычно классы). Стандартный стек: JUnit 5 (тестовый фреймворк) + Mockito (библиотека для мокинг-объектов).

1. Базовая структура теста

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class) // Интеграция JUnit с Mockito
class UserServiceTest {

    @Mock
    private UserRepository userRepository; // Зависимость, которую мы мокаем

    @InjectMocks
    private UserService userService; // Тестируемый сервис, куда внедряются моки

    @Test
    void getUserById_WhenUserExists_ReturnsUser() {
        // 1. Arrange (Подготовка): задаем поведение мок-объектов
        Long userId = 1L;
        User expectedUser = new User(userId, "testUser");
        when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));

        // 2. Act (Действие): вызываем тестируемый метод
        User actualUser = userService.getUserById(userId);

        // 3. Assert (Проверка): проверяем результат
        assertNotNull(actualUser);
        assertEquals(expectedUser.getId(), actualUser.getId());
        assertEquals(expectedUser.getUsername(), actualUser.getUsername());
        // Проверяем, что метод репозитория был вызван ровно один раз с нужным аргументом
        verify(userRepository, times(1)).findById(userId);
    }

    @Test
    void getUserById_WhenUserNotFound_ThrowsException() {
        Long userId = 999L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        assertThrows(EntityNotFoundException.class, () -> {
            userService.getUserById(userId);
        });
    }
}

2. Ключевые практики:

  • Изоляция: Тест не должен зависеть от внешних сервисов (БД, API). Используйте моки.
  • Читаемость: Следуйте паттерну Arrange-Act-Assert. Используйте понятные имена методов (getUserById_WhenUserNotFound_ThrowsException).
  • Детерминированность: Тест всегда должен давать одинаковый результат.
  • Покрытие: Пишите тесты для основных (happy path) и граничных (error cases) сценариев.
  • Использование @BeforeEach: Для инициализации общих данных перед каждым тестом.

3. Дополнительные инструменты:

  • @SpringBootTest: Для интеграционных тестов, поднимающих контекст Spring.
  • TestRestTemplate / WebTestClient: Для тестирования REST контроллеров.
  • @DataJpaTest: Для тестирования JPA-репозиториев с embedded БД.