# Saga scenario testing While [each handler in a saga can be tested using a unit test](/samples/unit-testing/index.md#testing-a-saga), it's often helpful to test an entire scenario involving multiple messages. The `TestableSaga` class enables this kind of scenario testing and supports the following: * Exercises the `ConfigureHowToFindSaga` method to ensure mappings are valid. * Emulates saga processing behavior in NServiceBus, including automatic correlation property assignment in saga data upon receiving the first message. * Stores timeouts internally, which can be triggered by [advancing time](#advancing-time). > [!NOTE] > Testing [custom finders](/nservicebus/sagas/saga-finding.md) and [saga not found handlers](/nservicebus/sagas/saga-not-found.md) is currently not supported. ## Example A simple scenario test of a `ShippingPolicy` saga, including timeouts: ```cs [Test] public async Task SampleSagaScenarioTest() { // Create the testable saga var testableSaga = new TestableSaga(); // Create input messages var orderId = Guid.NewGuid().ToString().Substring(0, 8); var orderPlaced = new OrderPlaced { OrderId = orderId }; var orderBilled = new OrderBilled { OrderId = orderId }; // Process OrderPlaced and make assertions on the result var placeResult = await testableSaga.Handle(orderPlaced); using (Assert.EnterMultipleScope()) { Assert.That(placeResult.Completed, Is.False); Assert.That(placeResult.FindPublishedMessage(), Is.Null); Assert.That(placeResult.FindTimeoutMessage(), Is.Null); } // Process OrderBilled and make assertions on the result var billResult = await testableSaga.Handle(orderBilled); using (Assert.EnterMultipleScope()) { Assert.That(billResult.Completed, Is.False); Assert.That(billResult.FindPublishedMessage(), Is.Null); Assert.That(billResult.FindTimeoutMessage(), Is.Not.Null); // Each result includes a snapshot of saga data after each message. // Snapshots can be asserted even after multiple operations have occurred. Assert.That(placeResult.SagaDataSnapshot.OrderId, Is.EqualTo(orderId)); Assert.That(placeResult.SagaDataSnapshot.Placed, Is.True); Assert.That(placeResult.SagaDataSnapshot.Billed, Is.False); } // Timeouts are stored and can be played by advancing time var noResults = await testableSaga.AdvanceTime(TimeSpan.FromMinutes(10)); // But that wasn't long enough Assert.That(noResults.Length, Is.EqualTo(0)); // Advance time more to get the timeout to fire var timeoutResults = await testableSaga.AdvanceTime(TimeSpan.FromHours(1)); Assert.That(timeoutResults, Has.Length.EqualTo(1)); var shipped = timeoutResults.First().FindPublishedMessage(); Assert.That(shipped.OrderId, Is.EqualTo(orderId)); } ``` ## Creating a testable saga Often, a testable saga can be created using just the type parameters: ```cs // For testing: public class MySaga : Saga var testableSaga = new TestableSaga(); ``` This assumes a parameterless constructor. If the saga requires injected services, a factory can be provided to create an instance per handled message: ```cs var testableSaga = new TestableSaga( sagaFactory: () => new MySaga(new InjectedService())); ``` By default, `CurrentTime` is initialized to `DateTime.UtcNow`, but a specific value can be set via the constructor. See [advancing time](#advancing-time) for details: ```cs var testableSaga = new TestableSaga( initialCurrentTime: new DateTime(2022, 01, 01, 0, 0, 0, DateTimeKind.Utc)); ``` ## Handling messages The testable saga mimics NServiceBus saga infrastructure. Each time it handles a message, it: 1. Instantiates a new saga instance. 2. Uses `ConfigureHowToFindSaga` to locate the matching saga data in internal storage. To handle a message: ```cs var handleResult = await testableSaga.Handle(new MyCommand()); ``` Optional parameters allow using a custom `TestableMessageHandlerContext` or providing custom headers: ```cs var customHandlerContext = new TestableMessageHandlerContext(); var customHeaders = new Dictionary(); var handleResult = await testableSaga.Handle(new MyCommand(), context: customHandlerContext, messageHeaders: customHeaders); ``` ## Handler results Handling a message returns a `HandleResult`, which contains: * `SagaId`: The `Guid` of the saga instance created or loaded. * `Completed`: Whether `MarkAsComplete()` was called. * `HandledMessage`: Type, headers, and body of the message handled. * `Context`: A [`TestableMessageHandlerContext`](/nservicebus/testing/index.md#testing-a-handler) with sent/published messages and other operations during handling. * `SagaDataSnapshot`: Copy of saga data after handling. * Helpers for locating specific messages in the `Context`: * `FindSentMessage()` * `FindPublishedMessage()` * `FindTimeoutMessage()` * `FindReplyMessage()` ## Advancing time `CurrentTime` represents a virtual clock. It defaults to the test start time but can be set in the [constructor](#creating-a-testable-saga). Timeouts are stored during message handling. Calling `AdvanceTime` processes due timeouts and returns an array of `HandleResult`: ```cs // Returns HandleResult[] var timeoutResults = await testableSaga.AdvanceTime(timeSpanToAdvance); DateTime newCurrentTime = testableSaga.CurrentTime; ``` Need custom `TestableMessageHandlerContext` per timeout? Use the overload: ```cs // Returns HandleResult[] var timeoutResults = await testableSaga.AdvanceTime(timeSpanToAdvance, provideContext: timeoutDetails => { // Provide custom context based on timeout details return new TestableMessageHandlerContext(); }); ``` ## Simulating external handlers Sagas often send commands to external handlers, expecting replies. These reply messages are [auto-correlated](/nservicebus/sagas/message-correlation.md#auto-correlation) using a saga ID header. Simulate such replies with `SimulateReply`: ```cs testableSaga.SimulateReply(step1Command => { return new Step1Response(); }); ``` The reply is enqueued internally, with the correlation header set. If you want to manually handle a reply instead: ```cs await testableSaga.HandleReply(previousHandleResult.SagaId, new Step1Response()); ``` You can also supply custom headers or a custom context: ```cs var customHandlerContext = new TestableMessageHandlerContext(); var customHeaders = new Dictionary(); await testableSaga.HandleReply(previousHandleResult.SagaId, new Step1Response(), context: customHandlerContext, messageHeaders: customHeaders); ``` ## Queued messages Any message sent/published that the saga handles is queued internally. This includes: * Saga sends/publishes of messages it also handles. * Messages produced by [external handler simulations](#simulating-external-handlers). You can inspect and assert against the message queue: ```cs bool hasMessages = testableSaga.HasQueuedMessages; int size = testableSaga.QueueLength; var nextMessage = testableSaga.QueuePeek(); ``` To process the next queued message: ```cs await testableSaga.HandleQueuedMessage(); var customContext = new TestableMessageHandlerContext(); await testableSaga.HandleQueuedMessage(context: customContext); ``` > [!NOTE] > Use `HandleQueuedMessage()` to test specific message ordering or simulate race conditions. Control whether timeouts or replies are processed first by choosing `AdvanceTime()` or `HandleQueuedMessage()` accordingly. ## Additional examples For more usage patterns, see the [NServiceBus.Testing saga tests](https://github.com/Particular/NServiceBus.Testing/tree/master/src/NServiceBus.Testing.Tests/Sagas).