Вы тестировали архитектуру, использующую паттерн Saga?

«Вы тестировали архитектуру, использующую паттерн Saga?» — вопрос из категории Архитектура, который задают на 24% собеседований AQA / Automation. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, я сталкивался с тестированием систем, построенных с использованием паттерна Saga (или Orchestration-based Saga) для управления распределенными транзакциями в микросервисной архитектуре. Основной фокус был на проверке согласованности данных и отказоустойчивости.

Ключевые аспекты тестирования:

  1. Счастливый путь (Happy Path): Проверка, что при успешном выполнении всех шагов (сервисов) саги система приходит в корректное целевое состояние.
  2. Компенсирующие транзакции (Compensation): Тестирование сценариев, когда один из шагов завершается ошибкой. Убеждался, что для всех уже выполненных шагов запускаются корректные компенсирующие действия (компенсации) и система откатывается в согласованное состояние.
  3. Идемпотентность: Проверка, что повторный вызов шага или компенсации (например, из-за таймаута сети) не приводит к побочным эффектам или нарушению консистенции данных.
  4. Восстановление после сбоев: Тестирование поведения саги при временной недоступности сервисов (retry logic) и корректности обработки таймаутов.

Пример интеграционного теста (концептуальный):

// Пример для Spring Boot / JUnit 5, тестирующий сценарий отката
@Test
void orderSaga_ShouldCompensate_WhenPaymentServiceFails() {
    // 1. Запускаем сагу создания заказа
    OrderSaga saga = startOrderSaga(testOrderData);

    // 2. Мокаем сервис оплаты, чтобы он симулировал сбой
    when(paymentServiceClient.process(any())).thenThrow(new ServiceUnavailableException());

    // 3. Выполняем сагу
    saga.execute();

    // 4. Проверяем результаты компенсации:
    // - Заказ должен иметь статус "CANCELLED"
    assertThat(orderRepository.findById(saga.getOrderId()))
        .isPresent().get().hasFieldOrPropertyWithValue("status", OrderStatus.CANCELLED);
    // - Должен был быть вызван компенсирующий вызов в сервис инвентаря
    verify(inventoryServiceClient).compensateReservation(saga.getOrderId());
    // - Пользователь должен быть уведомлен
    verify(notificationServiceClient).sendCancellationNotification(any());
}

Для таких тестов критически важно иметь хорошо изолированное тестовое окружение с заглушками (stubs/mocks) для всех вовлеченных сервисов.