← назад к разделу

Внешние зависимости — 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 и другие срезы.