Почему ваши Java-тесты ломаются (и как это исправить)
Вы написали идеальный код. Сервис работает, логика безупречна, а баги — только в чужих проектах. Но когда запускаете тесты... Там — ошибка подключения к БД, тут — внешний API не отвечает, а в третьем тесте — вообще NullPointerException, потому что конфигурация поднялась не так. Знакомо?
Проблема в том, что классические тесты зависят от внешнего окружения. База данных, REST-сервисы, файловая система — всё это делает тесты хрупкими, медленными и непредсказуемыми. Решение — mock-объекты и фикстуры. Это не просто модные слова, а инструменты, которые превращают хаос в порядок.
В этой статье мы разберёмся, как правильно использовать mock и фикстуры в Java-тестах на реальных примерах. Вы узнаете, чем stub отличается от mock, как настраивать фикстуры для сложных сценариев и почему Mockito — ваш лучший друг. Поехали!
Mock-объекты: симуляция без боли
Что такое mock и зачем он нужен?
Mock-объект — это имитация реального компонента, которая ведёт себя так, как вы скажете. Вместо того чтобы поднимать базу данных или вызывать внешний API, вы создаёте «фальшивку», которая возвращает заранее заданные данные.
Представьте: вы тестируете сервис, который отправляет email-уведомления. Для настоящего теста нужно поднять почтовый сервер, настроить SMTP, проверить очередь... А с mock-объектом вы просто проверяете, что сервис вызвал метод отправки с правильными параметрами. Быстро, чисто, контролируемо.
Mock vs Stub: в чём разница?
Новички часто путают эти понятия. Давайте расставим точки над i:
- Stub — возвращает фиксированные данные. Вы говорите: «При вызове метода getUser() верни объект User с именем "John"». Stub не проверяет, сколько раз его вызвали.
- Mock — не только возвращает данные, но и проверяет поведение. Вы можете сказать: «Убедись, что метод sendEmail() был вызван ровно один раз с конкретным адресом».
Проще говоря: stub — это «дай мне данные», mock — «дай мне данные и проверь, как меня используют». На практике чаще всего используют mock-объекты, так как они дают больше контроля.
Пишем первый mock с Mockito
Mockito — самая популярная библиотека для моков в Java. Она интегрируется с JUnit 5 и Spring Boot, а синтаксис настолько простой, что вы запомните его за пять минут.
Допустим, у вас есть сервис OrderService, который зависит от репозитория OrderRepository. Вместо реального репозитория мы создадим mock:
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Test
void testCreateOrder() {
// Создаём mock-объект
OrderService orderService = new OrderService(orderRepository);
// Настраиваем поведение mock
when(orderRepository.save(any(Order.class)))
.thenReturn(new Order(1L, "test@example.com", 100.0));
// Вызываем тестируемый метод
Order result = orderService.createOrder("test@example.com", 100.0);
// Проверяем результат
assertThat(result.getId()).isEqualTo(1L);
// Проверяем, что метод save был вызван ровно один раз
verify(orderRepository, times(1)).save(any(Order.class));
}
}
В этом примере мы:
- Создали mock для
OrderRepositoryс помощью аннотации@Mock. - Настроили, что при вызове
save()должен возвращаться заранее созданный заказ. - Проверили, что метод действительно был вызван (верификация).
Обратите внимание: мы не трогаем реальную базу данных. Тест летит за миллисекунды, а не за секунды.
Фикстуры: готовим данные для тестов
Что такое фикстуры и зачем они нужны?
Фикстуры (test fixtures) — это предопределённое состояние данных, которое нужно для выполнения теста. Например, если вы тестируете поиск товаров по категории, вам нужно, чтобы в базе уже были какие-то товары. Фикстуры решают эту задачу, создавая «подопытные» объекты до запуска теста.
Зачем это нужно? Во-первых, чтобы не дублировать код инициализации в каждом тесте. Во-вторых, чтобы гарантировать, что каждый тест стартует с одинаковыми данными — никаких сюрпризов от предыдущих запусков.
Способы создания фикстур в Java
Есть несколько подходов, от простых к сложным:
- Ручное создание в методе @BeforeEach — классика JUnit. Подходит для небольших проектов.
- Object Mother — паттерн, при котором вы создаёте фабричные методы для часто используемых объектов.
- Библиотеки для фикстур — например, Instancio или EasyRandom, которые генерируют объекты автоматически.
Рассмотрим каждый вариант на примере класса User с полями id, name, email и role.
Вариант 1: @BeforeEach (простой, но много кода)
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class UserServiceTest {
private User adminUser;
private User regularUser;
@BeforeEach
void setUp() {
adminUser = new User(1L, "Alice", "alice@example.com", Role.ADMIN);
regularUser = new User(2L, "Bob", "bob@example.com", Role.USER);
}
@Test
void testAdminCanDeleteUser() {
UserService userService = new UserService();
assertTrue(userService.canDeleteUser(adminUser));
}
@Test
void testRegularUserCannotDeleteUser() {
UserService userService = new UserService();
assertFalse(userService.canDeleteUser(regularUser));
}
}
Этот метод работает, но если у вас 10 классов с разными состояниями, код быстро разрастается.
Вариант 2: Object Mother (чище и переиспользуемо)
Создаём отдельный класс-фабрику:
public class UserFixture {
public static User createAdmin() {
return new User(1L, "Alice", "alice@example.com", Role.ADMIN);
}
public static User createRegularUser() {
return new User(2L, "Bob", "bob@example.com", Role.USER);
}
public static User createUserWithRole(Role role) {
return new User(null, "Default", "