Что будешь покрывать тестами при написании нового микросервиса
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Покрытие тестами при разработке нового микросервиса
Общее направление
При разработке нового микросервиса нужно покрывать тестами различные уровни архитектуры. Использую подход 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
Вывод
Для нового микросервиса нужно:
- Unit тесты — основной фокус (60-70%)
- Интеграционные тесты — critical paths (20-25%)
- API тесты — контракты (5-10%)
- Contract тесты — синхронизация с другими сервисами
- E2E тесты — важные юзер-флоу
На практике: писать тесты ДО кода (TDD), быстрые unit тесты, интеграционные на реальной БД в Docker.