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-тестов.