Ответ
Тестирование Saga критически важно, так как паттерн управляет сложными сценариями с откатами. Фокус должен быть на целостности процесса и надежности компенсаций.
Стратегия тестирования:
- Тестирование успешного сценария (Happy Path): Проверка, что все шаги Saga выполняются последовательно и система приходит в согласованное состояние.
- Тестирование откатов (Rollback Scenarios): Симуляция сбоя на каждом возможном шаге Saga. Убедиться, что все уже выполненные шаги корректно компенсируются в обратном порядке.
- Тестирование идемпотентности: Компенсирующие операции и, по возможности, сами шаги Saga должны быть идемпотентными (повторный вызов не должен ломать систему). Это защита от повторной доставки сообщений.
- Тестирование таймаутов и сетевых проблем: Проверка поведения Saga при недоступности сервиса, долгом ответе или разрыве соединения.
Практический пример теста на Python с использованием pytest и моков:
import pytest
from unittest.mock import Mock, patch
from my_app.saga import OrderSaga, PaymentService, InventoryService, NotificationService
# 1. Тест успешного выполнения
class TestOrderSagaHappyPath:
def test_saga_completes_successfully(self):
# Arrange
saga = OrderSaga(order_id=123)
payment_mock = Mock(spec=PaymentService)
inventory_mock = Mock(spec=InventoryService)
# ... моки других сервисов
# Act
result = saga.execute()
# Assert
assert result.is_success()
payment_mock.charge.assert_called_once_with(order_id=123)
inventory_mock.reserve.assert_called_once_with(order_id=123)
# Проверяем, что компенсации НЕ вызывались
payment_mock.refund.assert_not_called()
inventory_mock.release.assert_not_called()
# 2. Тест отката при ошибке на втором шаге
class TestOrderSagaCompensation:
def test_saga_compensates_on_failure(self):
# Arrange
saga = OrderSaga(order_id=456)
payment_mock = Mock(spec=PaymentService)
# Инвентарь выбросит исключение при резервировании
inventory_mock = Mock(spec=InventoryService)
inventory_mock.reserve.side_effect = Exception("Out of stock")
# Act
result = saga.execute()
# Assert
assert result.is_failure()
# Проверяем, что оплата прошла, но потом была отменена
payment_mock.charge.assert_called_once()
payment_mock.refund.assert_called_once_with(order_id=456) # Компенсация!
# Проверяем, что последующие шаги (например, уведомление) не запускались
# notification_mock.send.assert_not_called()
# 3. Тест идемпотентности компенсации
def test_compensation_is_idempotent(self):
# Симуляция ситуации, где сообщение о компенсации может прийти дважды
payment_mock = Mock(spec=PaymentService)
# Первый вызов refund работает, второй не должен ничего ломать
saga = OrderSaga(order_id=789)
# ... настройка сбоя и первый вызов execute
saga.execute()
# Имитируем повторный вызов компенсации
saga.compensate() # Внутри должен вызывать payment_mock.refund
# Убеждаемся, что refund был вызван, но логика внутри сервиса идемпотентна
# (например, проверяет статус перед действием).
# На уровне теста Saga мы проверяем, что повторный вызов не бросает исключений.
assert True # Если не упало — уже хорошо. Лучше проверить состояние БД.
Ключевые аспекты для проверки в интеграционных/Е2Е тестах:
- Согласованность данных: После отката Saga данные во всех затронутых сервисах должны вернуться в состояние, согласованное с бизнес-правилами (деньги возвращены, товары разрезервированы).
- Логирование и трассируемость: В логах должен оставаться четкий след (correlation ID) всех шагов и компенсаций для возможности отладки в продакшене.
- Восстановление после сбоя (Recovery): Тестирование механизма, который «добивает» прерванные Saga после перезапуска сервиса.
Ответ 18+ 🔞
Да ты посмотри, какая хуйня сложная вырисовалась! Сага, блядь. Не сказка, а паттерн такой, сука, который как змея кусает себя за хвост, а потом ещё и откатывается, если что не так. Тестировать это дело — это не хуй собачий, это надо с умом подходить, а то такую кашу в продакшене заваришь, что овердохуища компенсаций не хватит.
Главная мысль, которую в башке держать надо: проверяй, что весь процесс — цельный, а откаты — надёжные, как швейцарские часы, блядь. Если один шаг пизданулся, все предыдущие должны отмениться так, будто их и не было.
Как эту поебень тестировать, чтобы не обосраться:
- Счастливый путь (Happy Path): Ну тут всё просто, как три копейки. Запустил сагу — она пробежалась по всем шагам, как хорошая борзая, и всё сделала. Проверяешь, что система в итоге в том состоянии, в котором и должна быть. Никаких компенсаций, нихуя лишнего.
- Сценарии отката (Rollback): А вот тут начинается цирк, блядь. Нужно на каждом, блядь, шаге симулировать пиздец. Упала оплата? Откат. Упало резервирование товара? Откат, и оплата уже должна вернуться. Упала отправка письма? Откат, и товар разрезервирован, и деньги назад. Проверяешь, что компенсации сработали в обратном порядке — как шаги выполнялись.
- Идемпотентность: Это слово, блядь, выучить надо. Означает, что если ты вызовешь одну и ту же операцию (особенно компенсацию) дважды — ничего не сломается. Сообщения в очередях могут прилететь повторно, и система не должна от этого взорваться, как хитрая жопа. Компенсирующие операции обязаны быть идемпотентными.
- Таймауты и проблемы сети: А что будет, если сервис завис? Или сеть легла? Сага не должна висеть вечно. Проверяешь, как она реагирует на долгий ответ или полную недоступность.
Смотри, как это примерно в коде выглядит (Python, pytest):
import pytest
from unittest.mock import Mock, patch
from my_app.saga import OrderSaga, PaymentService, InventoryService, NotificationService
# 1. Тест, когда всё прошло гладко
class TestOrderSagaHappyPath:
def test_saga_completes_successfully(self):
# Подготовка (Arrange)
saga = OrderSaga(order_id=123)
payment_mock = Mock(spec=PaymentService) # Мок сервиса оплаты
inventory_mock = Mock(spec=InventoryService) # Мок сервиса запасов
# ... и так далее
# Действие (Act)
result = saga.execute()
# Проверка (Assert)
assert result.is_success() # Всё ок
payment_mock.charge.assert_called_once_with(order_id=123) # Деньги списались
inventory_mock.reserve.assert_called_once_with(order_id=123) # Товар зарезервирован
# Важно! Компенсации НЕ должны были вызваться
payment_mock.refund.assert_not_called()
inventory_mock.release.assert_not_called()
# 2. Тест, когда всё пошло по пизде на втором шаге
class TestOrderSagaCompensation:
def test_saga_compensates_on_failure(self):
# Подготовка
saga = OrderSaga(order_id=456)
payment_mock = Mock(spec=PaymentService)
# Сервис инвентаря сейчас кинет исключение, типа "нет на складе"
inventory_mock = Mock(spec=InventoryService)
inventory_mock.reserve.side_effect = Exception("Out of stock")
# Действие
result = saga.execute()
# Проверка
assert result.is_failure() # Сага упала
# Но! Оплата-то уже прошла...
payment_mock.charge.assert_called_once()
# ...а значит должна быть отменена! Вот она, компенсация!
payment_mock.refund.assert_called_once_with(order_id=456)
# И следующие шаги (типа уведомления) даже не стартовали
# notification_mock.send.assert_not_called()
# 3. Тест идемпотентности компенсации
def test_compensation_is_idempotent(self):
# Ситуация: сообщение о компенсации прилетело два раза
payment_mock = Mock(spec=PaymentService)
saga = OrderSaga(order_id=789)
# Настраиваем сбой, сага выполняется и запускает компенсацию
saga.execute()
# БАМ! Имитируем повторный вызов компенсации (вдруг сообщение продублировалось)
saga.compensate() # Внутри опять дергается payment_mock.refund
# Суть в том, что повторный вызов не должен ничего сломать.
# Сервис оплаты внутри себя должен проверять: "А не вернул ли я уже деньги?"
# Для теста саги главное — что не вылетело исключение.
assert True # Не упало — уже хорошо. В реальности смотри состояние БД.
А для интеграционных или Е2Е тестов смотри сюда, что важно:
- Согласованность данных, ёпта: После всей этой свистопляски с откатами данные во всех сервисах должны быть как будто ничего и не происходило. Деньги — на месте, товары — свободны. Никаких призрачных списаний или вечных резервов.
- Логи и трассировка: В логах должен быть ясный след, как у слона в посудной лавке. По
correlation_idможно восстановить всю цепочку: что сделали, что откатили, где накосячили. Без этого в продакшене — пиши пропало, будешь как слепой щенок тыкаться. - Восстановление после пиздеца (Recovery): А что если сервис с сагой упал посередине процесса? А потом поднялся. Должен быть механизм, который поднимет все незавершённые саги и доведёт их до ума — либо выполнит, либо, блядь, откатит. Это тоже надо тестировать, иначе они так и будут висеть мёртвым грузом.