Внешние зависимости — HTTP-сервисы, почтовые провайдеры, платёжные шлюзы — делают тесты медленными и хрупкими. В этой статье разберём, когда нужен мок (заглушка), а когда он только мешает, и как тестировать HTTP-вызовы с WireMock.
Когда мок уместен
Мок — объект, который притворяется настоящим: принимает вызовы и возвращает заранее заданные ответы. Mockito — стандартный инструмент для создания моков в Java-тестах.
Короткая формула: мок уместен на внешней границе — там, где заканчивается ваш код и начинается чужой.
Хорошие кандидаты для мока: HTTP-клиент к внешнему сервису, SMS-шлюз, email-провайдер. В unit-тесте достаточно убедиться, что ваш класс правильно реагирует на ответ зависимости — поднимать реальный сервер ради этого излишне.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentClient paymentClient;
@InjectMocks
private OrderService orderService;
@Test
void failsWhenPaymentDeclined() {
when(paymentClient.charge(any())).thenReturn(PaymentResult.DECLINED);
assertThrows(PaymentDeclinedException.class,
() -> orderService.place(orderRequest()));
}
}
Когда мок вредит
Короткая формула: мокать свой код — значит тестировать ожидания о поведении, а не само поведение.
Если мок стоит между двумя вашими классами (например, OrderService и OrderRepository), тест проходит даже при неправильном взаимодействии. Поменяйте сигнатуру или смысл метода — мок «не заметит», тест всё равно зеленеет.
Правило: моки только на внешних границах. Для своего Repository — лучше Testcontainers с реальной базой: запуск дороже, но тест честнее.
WireMock: HTTP-заглушка вместо реального сервиса
Когда ваш сервис вызывает внешний HTTP-сервис, поднять реальный сервер в тестах невозможно. WireMock запускает локальный HTTP-сервер и возвращает настроенные ответы.
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class PaymentGatewayClientTest {
@Autowired
private PaymentGatewayClient client;
@Test
void returnsDeclinedOnHttp402() {
stubFor(post(urlEqualTo("/charge"))
.willReturn(aResponse()
.withStatus(402)
.withHeader("Content-Type", "application/json")
.withBody("{\"status\":\"DECLINED\"}")));
PaymentResult result = client.charge(new ChargeRequest("card_123", 500));
assertThat(result).isEqualTo(PaymentResult.DECLINED);
}
}
@AutoConfigureWireMock(port = 0) — Spring выбирает свободный порт и подставляет его в свойства приложения автоматически. Клиент обращается к localhost, а не к реальному сервису.
WireMock позволяет имитировать задержку (withFixedDelay), разрывы соединения, последовательность ответов — полезно для проверки логики повторных попыток и таймаутов.
Изоляция тестовых данных
Тесты должны быть независимы: порядок запуска не должен влиять на результат. Три основных подхода.
Транзакционный откат — каждый тест выполняется в транзакции, которая откатывается после. Подходит, когда не нужно проверять поведение при commit.
@SpringBootTest
@Transactional
class ProductRepositoryTest {
// база данных чистая для каждого теста
}
Очистка через @Sql — явно сбрасывать состояние перед тестом или набором тестов. Медленнее, зато честно покрывает несколько транзакций.
@Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Test
void createsOrder() { ... }
Уникальные идентификаторы — генерировать уникальный UUID прямо в тесте, чтобы разные тесты не конкурировали за одни и те же записи.
Детерминированность: время и случайность
Тест с LocalDateTime.now() внутри проверяемого кода — недетерминированный: каждый прогон возвращает другой результат, и точную проверку не написать. Решение — Clock из java.time.
// Конфигурация: бин Clock в основном контексте
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
// Сервис принимает Clock через конструктор
public class OrderService {
private final Clock clock;
private LocalDateTime now() {
return LocalDateTime.now(clock);
}
}
// Тест подставляет фиксированный Clock
@Test
void stampsOrderWithCreationTime() {
Clock fixed = Clock.fixed(Instant.parse("2025-01-15T10:00:00Z"), ZoneOffset.UTC);
var service = new OrderService(fixed, repository);
Order order = service.place(orderRequest());
assertThat(order.createdAt()).isEqualTo(LocalDateTime.of(2025, 1, 15, 10, 0));
}
То же касается генераторов случайных чисел: передавайте Random с фиксированным seed через конструктор, не создавайте new Random() внутри метода.
Коротко
- Моки уместны только на внешних границах (HTTP-клиент, email, платёжный шлюз); мокать свой код делает тест хрупким.
- WireMock поднимает локальный HTTP-сервер и заменяет реальный внешний сервис без сетевых вызовов.
- Testcontainers с реальной базой надёжнее, чем мок-репозиторий.
- Изолируйте тестовые данные: транзакционный откат,
@Sqlили уникальные идентификаторы в каждом тесте. LocalDateTime.now()внутри кода — источник недетерминированности; заменяйте наClock, внедряемый через конструктор.
Что почитать дальше
- Пирамида тестирования — какой уровень тестов за что отвечает.
- Интеграционное тестирование — Testcontainers,
@SpringBootTest, тесты со слоем. - Тестирование в Spring —
@WebMvcTest,@DataJpaTestи другие срезы.