Обзор Event-Driven архитектуры
В event-driven архитектуре (EDA) сервисы общаются, продюсируя и потребляя события, а не делая прямые API-вызовы. Когда что-то происходит в Сервисе A (пользователь оформляет заказ), он публикует событие. Другие сервисы реагируют на это событие независимо.
Такой подход обеспечивает слабую связанность, высокую масштабируемость и устойчивость — но вносит сложности тестирования, отсутствующие в синхронных системах: eventual consistency, порядок событий, дублирование доставки и сложные режимы отказов.
Event Sourcing
Вместо хранения текущего состояния сущности, event sourcing хранит последовательность событий, приведших к этому состоянию.
Традиционный подход (на основе состояния):
Заказ #123: status=отправлен, total=99.99, items=[A, B]
Подход event sourcing:
Событие 1: OrderCreated { orderId: 123, items: [A, B] }
Событие 2: PaymentReceived { orderId: 123, amount: 99.99 }
Событие 3: OrderShipped { orderId: 123, trackingNumber: "ABC123" }
Текущее состояние получается воспроизведением всех событий по порядку.
Тестирование Event Sourcing
Тест 1: Реконструкция состояния Дана последовательность событий — проверьте, что текущее состояние aggregate корректно.
test('состояние заказа после создания и оплаты', () => {
const events = [
{ type: 'OrderCreated', data: { orderId: '123', items: ['A', 'B'], total: 99.99 } },
{ type: 'PaymentReceived', data: { orderId: '123', amount: 99.99 } },
];
const order = replayEvents(new Order(), events);
expect(order.status).toBe('paid');
expect(order.total).toBe(99.99);
expect(order.items).toEqual(['A', 'B']);
});
Тест 2: Порядок событий важен Применение событий в другом порядке должно давать другое состояние (или быть отклонено).
Тест 3: Согласованность snapshot-ов Если система использует snapshot-ы, проверьте, что состояние из snapshot + последующие события равно состоянию при воспроизведении всех событий.
CQRS (Command Query Responsibility Segregation)
CQRS разделяет модель записи (commands) и модель чтения (queries). Commands порождают события, обновляющие write store. Projector обрабатывает события для обновления оптимизированных для чтения представлений.
Command → Write Model → Events → Projector → Read Model
Тестирование CQRS
Тестирование стороны команд:
- Отправка валидной команды порождает ожидаемые события.
- Отправка невалидной команды отклоняется с корректной ошибкой.
- Команды применяют бизнес-правила (нельзя отправить неоплаченный заказ).
Тестирование стороны чтения (projections):
- После обработки событий read model отражает корректное состояние.
- Projection-ы корректно обрабатывают события не по порядку.
- Пересборка projection-ов даёт тот же результат, что и инкрементальные обновления.
Тестирование разрыва согласованности: После успеха команды есть задержка до обновления read model.
Паттерн Saga
Saga-и координируют распределённые транзакции между сервисами. Есть два типа:
Saga на основе хореографии
Каждый сервис слушает события и решает, что делать. Без центрального координатора.
OrderService: OrderCreated →
PaymentService: (слушает) → PaymentProcessed →
InventoryService: (слушает) → StockReserved →
ShippingService: (слушает) → ShipmentCreated
Тестирование хореографии:
- Запустите начальное событие и проверьте завершение всей цепочки.
- Симулируйте сбой на каждом шаге и проверьте запуск компенсирующих событий.
- Проверьте сценарии таймаутов — что происходит, если сервис не отвечает?
Saga на основе оркестрации
Центральный оркестратор координирует шаги saga.
Тестирование оркестрации:
- Happy path: Все шаги успешны → saga завершается.
- Сбой на каждом шаге: Проверьте корректное выполнение компенсирующих действий.
- Crash/перезапуск оркестратора: Проверьте возобновление saga с места остановки.
Тестирование Eventual Consistency
Самая сложная часть тестирования event-driven систем — проверка, что сервисы в итоге достигнут согласованного состояния.
Паттерн Polling
async function waitForConsistency(checkFn, timeout = 10000, interval = 500) {
const start = Date.now();
while (Date.now() - start < timeout) {
const result = await checkFn();
if (result) return result;
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error('Превышен таймаут согласованности');
}
test('заказ появляется в read model после создания', async () => {
await commandBus.send(new CreateOrderCommand({ items: ['A'] }));
const order = await waitForConsistency(async () => {
const result = await readModel.getOrder('123');
return result?.status === 'created' ? result : null;
});
expect(order.items).toEqual(['A']);
});
Тестирование идемпотентности
Проверьте, что обработка одного события дважды даёт тот же результат:
test('событие оплаты идемпотентно', async () => {
const event = { type: 'PaymentReceived', orderId: '123', amount: 99.99 };
await processEvent(event);
await processEvent(event); // дублирование доставки
const payments = await db.getPayments('123');
expect(payments.length).toBe(1); // не 2
expect(payments[0].amount).toBe(99.99);
});
Эволюция схемы событий
По мере развития систем схемы событий меняются. Потребители должны обрабатывать как старые, так и новые форматы.
Упражнение: Тестирование event-driven системы
Спроектируйте и выполните тесты для event-driven системы электронной коммерции.
Описание системы
Система состоит из четырёх сервисов, общающихся через события:
OrderService → PaymentService → InventoryService → NotificationService
События:
OrderCreated— публикуется при оформлении заказаPaymentProcessed— публикуется при подтверждении оплатыPaymentFailed— публикуется при отклонении оплатыStockReserved— публикуется при резервировании запасовStockInsufficient— публикуется при недостатке запасовOrderCancelled— компенсирующее событие при сбое шагаRefundInitiated— компенсирующее событие при необходимости возврата
Задание 1: Тест Happy Path
Напишите тест, проверяющий полный поток:
- Опубликуйте
OrderCreatedс деталями заказа. - Дождитесь появления
PaymentProcessed. - Дождитесь появления
StockReserved. - Проверьте, что статус заказа в read model — “confirmed”.
- Проверьте, что запасы уменьшились.
Задание 2: Тесты компенсации
Для каждой точки сбоя проверьте корректный откат saga:
Сценарий A: Сбой оплаты
- Опубликуйте
OrderCreated. - PaymentService публикует
PaymentFailed. - Проверьте публикацию
OrderCancelled. - Проверьте статус заказа “cancelled” в read model.
Сценарий B: Недостаточно запасов
- Опубликуйте
OrderCreated. - PaymentService публикует
PaymentProcessed. - InventoryService публикует
StockInsufficient. - Проверьте публикацию
RefundInitiated(компенсация оплаты). - Проверьте публикацию
OrderCancelled.
Задание 3: Тест идемпотентности
- Опубликуйте одно и то же событие
OrderCreatedдважды (симуляция дублирования). - Проверьте, что в базе создан только один заказ.
- Проверьте, что была только одна попытка оплаты.
Задание 4: Тест порядка событий
- Опубликуйте
PaymentProcessedраньшеOrderCreated(не по порядку). - Проверьте, что система обрабатывает это корректно.
Задание 5: Проверка Eventual Consistency
- Создайте заказ через endpoint команды.
- Немедленно запросите read model — он может быть ещё не обновлён.
- Делайте polling read model до появления заказа (с таймаутом).
- Зафиксируйте задержку согласованности.
- Повторите 10 раз и рассчитайте среднюю задержку.
Результаты
- Тестовый код для каждого сценария с assertions.
- Отчёт по таймингам с задержками eventual consistency.
- Документация обнаруженных race condition-ов или пограничных случаев.