Gymterview
middle

Как тестировать микросервисы?

Тестирование микросервисов строится на основе пирамиды тестирования, адаптированной для распределённой архитектуры: unit-тесты (много), component-тесты, contract-тесты (между сервисами), integration-тесты и E2E-тесты (мало).

Пример
         ╱  E2E тесты  ╲         — мало (дорогие, хрупкие)
        ╱ Integration    ╲        — средне
       ╱ Contract tests   ╲       — много (между сервисами)
      ╱ Component tests    ╲      — средне (один сервис целиком)
     ╱ Unit tests           ╲     — очень много (быстрые)
1. Unit-тесты: тестирование бизнес-логики
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
    @Mock private PaymentRepository paymentRepository;
    @Mock private CustomerClient customerClient;
    @InjectMocks private PaymentService paymentService;

    @Test
    void shouldRejectPaymentWhenInsufficientFunds() {
        when(paymentRepository.findById(1L))
            .thenReturn(Optional.of(new Payment(1L, new BigDecimal("100"))));

        assertThrows(InsufficientFundsException.class, () ->
            paymentService.withdraw(1L, new BigDecimal("200")));
    }
}
2. Component-тесты: один сервис с поднятым контекстом и тестовой БД
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class PaymentServiceComponentTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldCreatePayment() {
        PaymentRequest request = new PaymentRequest(BigDecimal.valueOf(1000), "RUB");

        ResponseEntity<PaymentResponse> response = restTemplate
            .postForEntity("/api/payments", request, PaymentResponse.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getStatus()).isEqualTo("CREATED");
    }
}
3. Contract Testing с Pact
// Consumer-сторона (сервис, который вызывает payment-service)
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "payment-service")
class PaymentClientContractTest {

    @Pact(consumer = "order-service")
    public V4Pact createPact(PactDslWithProvider builder) {
        return builder
            .given("Платёж с ID 1 существует")
            .uponReceiving("Запрос платежа по ID")
            .path("/api/payments/1")
            .method("GET")
            .willRespondWith()
            .status(200)
            .body(newJsonBody(body -> {
                body.numberType("id", 1L);
                body.stringType("status", "COMPLETED");
                body.decimalType("amount", 1000.00);
            }).build())
            .toPact(V4Pact.class);
    }

    @Test
    @PactTestFor(pactMethod = "createPact")
    void shouldGetPayment(MockServer mockServer) {
        PaymentClient client = new PaymentClient(mockServer.getUrl());
        PaymentResponse payment = client.getPayment(1L);

        assertThat(payment.getStatus()).isEqualTo("COMPLETED");
    }
}

// Provider-сторона (payment-service проверяет, что соответствует контракту)
@Provider("payment-service")
@PactBroker(url = "http://pact-broker:9292")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PaymentProviderContractTest {

    @TestTarget
    public final Target target = new SpringBootHttpTarget();

    @State("Платёж с ID 1 существует")
    void setupPaymentExists() {
        paymentRepository.save(new Payment(1L, BigDecimal.valueOf(1000), "COMPLETED"));
    }
}
4. Integration-тесты с WireMock
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class PaymentServiceIntegrationTest {

    @Test
    void shouldCallCustomerServiceAndProcessPayment() {
        stubFor(get(urlEqualTo("/api/customers/1"))
            .willReturn(aResponse()
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {"id": 1, "name": "Иванов", "status": "ACTIVE"}
                    """)));

        PaymentResponse result = paymentService.processPayment(
            new PaymentRequest(1L, BigDecimal.valueOf(5000)));

        assertThat(result.getStatus()).isEqualTo("COMPLETED");
        verify(getRequestedFor(urlEqualTo("/api/customers/1")));
    }
}

На собеседовании: ключевое отличие от монолита — contract testing. Именно оно гарантирует, что API между сервисами не рассинхронизируется. Упомяните Pact и пирамиду тестов. Частая ошибка — пытаться покрыть всё E2E-тестами вместо contract-тестов.