﻿# Saga scenario testing

<!-- Version variant: testing_9; default: [/nservicebus/testing/saga-scenario-testing.md](/nservicebus/testing/saga-scenario-testing.md) -->


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:

<!-- snippet: BasicScenarioTest -->

```cs
[Test]
public async Task SampleSagaScenarioTest()
{
    // Create the testable saga
    var testableSaga = new TestableSaga<ShippingPolicy, ShippingPolicyData>();

    // 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<OrderShipped>(), Is.Null);
        Assert.That(placeResult.FindTimeoutMessage<ShippingDelay>(), 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<OrderShipped>(), Is.Null);
        Assert.That(billResult.FindTimeoutMessage<ShippingDelay>(), 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<OrderShipped>();
    Assert.That(shipped.OrderId, Is.EqualTo(orderId));
}
```

<!-- endsnippet -->

## Creating a testable saga

Often, a testable saga can be created using just the type parameters:

<!-- snippet: TestableSagaCtor -->

```cs
// For testing: public class MySaga : Saga<MyData>
var testableSaga = new TestableSaga<MySaga, MyData>();
```

<!-- endsnippet -->

This assumes a parameterless constructor. If the saga requires injected services, a factory can be provided to create an instance per handled message:

<!-- snippet: TestableSagaCtorFactory -->

```cs
var testableSaga = new TestableSaga<MySaga, MyData>(
    sagaFactory: () => new MySaga(new InjectedService()));
```

<!-- endsnippet -->

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:

<!-- snippet: TestableSagaCtorTime -->

```cs
var testableSaga = new TestableSaga<MySaga, MyData>(
    initialCurrentTime: new DateTime(2022, 01, 01, 0, 0, 0, DateTimeKind.Utc));
```

<!-- endsnippet -->

## 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:

<!-- snippet: TestableSagaSimpleHandle -->

```cs
var handleResult = await testableSaga.Handle(new MyCommand());
```

<!-- endsnippet -->

Optional parameters allow using a custom `TestableMessageHandlerContext` or providing custom headers:

<!-- snippet: TestableSagaHandleParams -->

```cs
var customHandlerContext = new TestableMessageHandlerContext();
var customHeaders = new Dictionary<string, string>();

var handleResult = await testableSaga.Handle(new MyCommand(), context: customHandlerContext, messageHeaders: customHeaders);
```

<!-- endsnippet -->

## 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<TMessage>()`
  * `FindPublishedMessage<TMessage>()`
  * `FindTimeoutMessage<TMessage>()`
  * `FindReplyMessage<TMessage>()`

## 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`:

<!-- snippet: TestableSagaAdvanceTime -->

```cs
// Returns HandleResult[]
var timeoutResults = await testableSaga.AdvanceTime(timeSpanToAdvance);

DateTime newCurrentTime = testableSaga.CurrentTime;
```

<!-- endsnippet -->

Need custom `TestableMessageHandlerContext` per timeout? Use the overload:

<!-- snippet: TestableSagaAdvanceTimeParams -->

```cs
// Returns HandleResult[]
var timeoutResults = await testableSaga.AdvanceTime(timeSpanToAdvance,
    provideContext: timeoutDetails =>
    {
        // Provide custom context based on timeout details
        return new TestableMessageHandlerContext();
    });
```

<!-- endsnippet -->

## 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`:

<!-- snippet: TestableSagaSimulateReply -->

```cs
testableSaga.SimulateReply<DoStep1, Step1Response>(step1Command =>
{
    return new Step1Response();
});
```

<!-- endsnippet -->

The reply is enqueued internally, with the correlation header set. If you want to manually handle a reply instead:

<!-- snippet: TestableSagaHandleReply -->

```cs
await testableSaga.HandleReply(previousHandleResult.SagaId, new Step1Response());
```

<!-- endsnippet -->

You can also supply custom headers or a custom context:

<!-- snippet: TestableSagaHandleReplyParams -->

```cs
var customHandlerContext = new TestableMessageHandlerContext();
var customHeaders = new Dictionary<string, string>();

await testableSaga.HandleReply(previousHandleResult.SagaId, new Step1Response(),
    context: customHandlerContext,
    messageHeaders: customHeaders);
```

<!-- endsnippet -->

## 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:

<!-- snippet: TestableSagaQueueOperations -->

```cs
bool hasMessages = testableSaga.HasQueuedMessages;
int size = testableSaga.QueueLength;
var nextMessage = testableSaga.QueuePeek();
```

<!-- endsnippet -->

To process the next queued message:

<!-- snippet: TestableSagaHandleQueuedMessage -->

```cs
await testableSaga.HandleQueuedMessage();

var customContext = new TestableMessageHandlerContext();
await testableSaga.HandleQueuedMessage(context: customContext);
```

<!-- endsnippet -->

> [!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).
