← Назад к вопросам

Что будешь покрывать тестами при написании нового микросервиса

1.7 Middle🔥 191 комментариев
#REST API и микросервисы#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Покрытие тестами при разработке нового микросервиса

Общее направление

При разработке нового микросервиса нужно покрывать тестами различные уровни архитектуры. Использую подход Test Pyramid: много unit тестов, меньше интеграционных, еще меньше E2E.

Уровни тестирования

1. Unit тесты (60% - основной фокус)

Тестируют отдельные компоненты в изоляции:

// Тест бизнес-логики
@Test
void shouldCalculateDiscountCorrectly() {
    // Arrange
    Order order = new Order(price = 100, quantity = 2);
    DiscountService discountService = new DiscountService();
    
    // Act
    BigDecimal discount = discountService.calculateDiscount(order);
    
    // Assert
    assertEquals(new BigDecimal("10.00"), discount);
}

// Тест сервиса с mock
@Test
void shouldCreateOrderSuccessfully() {
    // Mock external dependency
    PaymentGateway paymentGateway = mock(PaymentGateway.class);
    when(paymentGateway.charge(any())).thenReturn(true);
    
    OrderService orderService = new OrderService(paymentGateway);
    
    // Act
    Order order = orderService.createOrder(100.0, "user-123");
    
    // Assert
    assertNotNull(order.getId());
    assertTrue(order.isPaid());
    verify(paymentGateway).charge(any());
}

Что тестировать:

  • Вычисления и логика (скидки, налоги, комиссии)
  • Валидация входных данных
  • Обработка ошибок и edge cases
  • Преобразование данных
  • Кэширование

2. Интеграционные тесты (25% - critical paths)

Тестируют взаимодействие компонентов:

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class OrderServiceIntegrationTest {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private PaymentService paymentService;
    
    @Test
    @Transactional
    void shouldPersistOrderAfterPayment() {
        // Act
        Order order = orderService.createOrderWithPayment(
            new CreateOrderRequest("user-123", 150.0)
        );
        
        // Assert
        Order saved = orderRepository.findById(order.getId()).orElseThrow();
        assertEquals("user-123", saved.getUserId());
        assertEquals(150.0, saved.getAmount());
        assertEquals(OrderStatus.PAID, saved.getStatus());
    }
}

Что тестировать:

  • Сохранение в БД (через @SpringBootTest)
  • Транзакции и откаты
  • Cascade операции
  • Взаимодействие с внешними сервисами (HTTP, message queue)
  • Cache invalidation

3. API тесты (10% - контракты)

Тестируют HTTP endpoints:

@SpringBootTest
@AutoConfigureMockMvc
public class OrderControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private OrderService orderService;
    
    @Test
    void shouldReturnOrderDetails() throws Exception {
        // Arrange
        Order order = new Order("order-123", 100.0);
        when(orderService.getOrder("order-123")).thenReturn(order);
        
        // Act & Assert
        mockMvc.perform(get("/api/v1/orders/order-123")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value("order-123"))
            .andExpect(jsonPath("$.amount").value(100.0));
    }
    
    @Test
    void shouldValidateInput() throws Exception {
        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"amount\": -10}"))  // Invalid
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.error").exists());
    }
}

Что тестировать:

  • Валидация запроса (field validation)
  • Правильные HTTP коды ответов
  • Структура JSON ответа
  • Обработка ошибок API

4. Contract тесты (5% - для микросервисов)

Тестируют контракт с другими сервисами:

@SpringBootTest
@PactTestFor(providerName = "OrderService", port = "8080")
public class OrderProviderPactTest {
    
    @State("an order exists")
    void setupOrder() {
        Order order = new Order("123", 100.0);
        // Сохраняем в БД для теста
    }
    
    @Pact(consumer = "PaymentService")
    public V4Pact createPactWithPaymentService(
            PactBuilder builder) {
        return builder
            .given("an order exists")
            .uponReceiving("a request for order status")
            .path("/api/v1/orders/123")
            .method("GET")
            .willRespondWith()
            .status(200)
            .body(someJson())
            .toPact(V4Pact.class);
    }
}

Покрытие тестами по слоям архитектуры

Domain Layer (100% coverage)

@Test
void shouldNotCreateOrderWithNegativeAmount() {
    assertThrows(IllegalArgumentException.class, () -> {
        new Order("user-1", -100.0);
    });
}

@Test
void shouldCalculateTotalWithTax() {
    Order order = new Order("user-1", 100.0);
    assertEquals(120.0, order.getTotalWithTax());
}

Application Layer (80-90% coverage)

@Test
void shouldThrowExceptionIfPaymentFails() {
    PaymentGateway paymentGateway = mock(PaymentGateway.class);
    when(paymentGateway.charge(any())).thenThrow(
        new PaymentException("Card declined")
    );
    
    OrderService service = new OrderService(paymentGateway);
    
    assertThrows(PaymentException.class, () -> {
        service.createOrder(100.0, "user-123");
    });
}

Infrastructure Layer (50-70% coverage)

@SpringBootTest
void shouldPersistOrderToDatabase() {
    Order order = new Order("user-1", 100.0);
    Order saved = orderRepository.save(order);
    
    assertNotNull(saved.getId());
    assertEquals(order.getAmount(), saved.getAmount());
}

@Test
void shouldHandleDatabaseConnectionFailure() {
    // Test with @DataJpaTest and embedded database
}

Специальные случаи для микросервисов

1. Тестирование асинхронных операций

@Test
void shouldProcessOrderAsync() throws Exception {
    // Arrange
    Order order = new Order("user-1", 100.0);
    
    // Act
    CompletableFuture<OrderResult> future = 
        orderService.processOrderAsync(order);
    
    // Assert
    OrderResult result = future.get(5, TimeUnit.SECONDS);
    assertEquals(OrderStatus.PROCESSED, result.getStatus());
}

2. Тестирование обработчиков сообщений

@SpringBootTest
@EmbeddedKafka(brokerProperties = {"listeners=PLAINTEXT://localhost:9092"})
public class OrderEventListenerTest {
    
    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;
    
    @Autowired
    private OrderRepository repository;
    
    @Test
    void shouldProcessOrderCreatedEvent() throws InterruptedException {
        // Act
        OrderEvent event = new OrderEvent("order-123", 100.0);
        kafkaTemplate.send("order-events", event);
        
        // Assert
        Thread.sleep(1000);  // Wait for message processing
        assertTrue(repository.findById("order-123").isPresent());
    }
}

3. Тестирование Circuit Breaker

@Test
void shouldFallbackWhenExternalServiceIsDown() {
    // Mock PaymentGateway to always fail
    PaymentGateway paymentGateway = mock(PaymentGateway.class);
    when(paymentGateway.charge(any()))
        .thenThrow(new TimeoutException());
    
    OrderService service = new OrderService(paymentGateway);
    
    // Circuit breaker should activate after 5 failures
    for (int i = 0; i < 5; i++) {
        try {
            service.createOrder(100.0, "user-" + i);
        } catch (CircuitBreakerOpenException e) {
            // Expected
        }
    }
    
    // 6th call should fail immediately (circuit open)
    assertThrows(CircuitBreakerOpenException.class, () -> {
        service.createOrder(100.0, "user-6");
    });
}

4. Тестирование распределенных транзакций

@Test
void shouldRollbackSagaIfPaymentFails() {
    // Arrange
    Order order = createOrder(100.0);
    PaymentGateway.failNext();  // Payment will fail
    
    // Act
    assertThrows(SagaRollbackException.class, () -> {
        orderService.executeOrderSaga(order);
    });
    
    // Assert - confirm compensation logic ran
    assertEquals(OrderStatus.CANCELLED, order.getStatus());
    assertFalse(paymentRepository.exists(order.getId()));
}

Инструменты и практики

Testing Framework:

// JUnit 5 + AssertJ
import org.junit.jupiter.api.Test;
import org.assertj.core.api.Assertions.*;

@Test
void shouldBeMoreReadable() {
    Order order = new Order("user-1", 100.0);
    assertThat(order.getAmount())
        .isPositive()
        .isEqualTo(100.0);
}

Mocking & Stubbing:

// Mockito
PaymentGateway paymentGateway = mock(PaymentGateway.class);
when(paymentGateway.charge(any())).thenReturn(true);
verify(paymentGateway, times(1)).charge(any());

// Or with annotations
@Mock
private PaymentGateway paymentGateway;

@InjectMocks
private OrderService orderService;

Parameterized Tests:

@ParameterizedTest
@CsvSource({
    "100.0, 10.0",
    "200.0, 20.0",
    "50.0, 5.0"
})
void shouldCalculateDiscountForDifferentAmounts(
        Double amount, Double expectedDiscount) {
    assertEquals(expectedDiscount, 
        discountService.calculate(amount));
}

Целевой охват тестами

Unit tests:         ~70-80% кода
Integration tests:  ~50% critical paths
API tests:          ~30% endpoints
E2E tests:          ~5-10% user flows
────────────────────────────────────
Общий охват:        ~75-80% (goal for production)

Чек-лист покрытия тестами для микросервиса

  • Domain entities: 100% покрытие
  • Services: 80%+ покрытие
  • Repositories: интеграционные тесты
  • Controllers: API контракты
  • Error handling: все исключения
  • Edge cases: null, empty, invalid input
  • Async operations: CompletableFuture, Reactive
  • Database transactions: @Transactional
  • External services: mocked in unit, real in integration
  • Kafka/RabbitMQ: @EmbeddedKafka
  • Caching: hit/miss scenarios
  • Distributed tracing: context propagation

Вывод

Для нового микросервиса нужно:

  1. Unit тесты — основной фокус (60-70%)
  2. Интеграционные тесты — critical paths (20-25%)
  3. API тесты — контракты (5-10%)
  4. Contract тесты — синхронизация с другими сервисами
  5. E2E тесты — важные юзер-флоу

На практике: писать тесты ДО кода (TDD), быстрые unit тесты, интеграционные на реальной БД в Docker.

Что будешь покрывать тестами при написании нового микросервиса | PrepBro