|
1 | 1 | ---
|
2 | 2 | title: Saga scenario testing
|
3 | 3 | summary: Develop service layers and long-running processes using test-driven development.
|
4 |
| -reviewed: 2023-06-19 |
| 4 | +reviewed: 2025-05-30 |
5 | 5 | component: Testing
|
6 | 6 | versions: '[7.4,)'
|
7 | 7 | related:
|
8 | 8 | ---
|
9 | 9 |
|
10 |
| -While [each handler in a saga can be tested using a unit test](/samples/unit-testing/#testing-a-saga), often it is helpful to test an entire scenario involving multiple messages. |
| 10 | +While [each handler in a saga can be tested using a unit test](/samples/unit-testing/#testing-a-saga), it's often helpful to test an entire scenario involving multiple messages. |
11 | 11 |
|
12 |
| -The `TestableSaga` class allows this type of scenario testing and supports the following features: |
| 12 | +The `TestableSaga` class enables this kind of scenario testing and supports the following: |
13 | 13 |
|
14 |
| -* Exercises the `ConfigureHowToFindSaga` method to ensure that mappings are valid. |
15 |
| -* Emulates how sagas are processed by NServiceBus, including automatically setting the correlation property in the saga data when the first message is received. |
| 14 | +* Exercises the `ConfigureHowToFindSaga` method to ensure mappings are valid. |
| 15 | +* Emulates saga processing behavior in NServiceBus, including automatic correlation property assignment in saga data upon receiving the first message. |
16 | 16 | * Stores timeouts internally, which can be triggered by [advancing time](#advancing-time).
|
17 | 17 |
|
18 | 18 | ## Example
|
19 | 19 |
|
20 |
| -Here's a simple sample of a scenario test of a `ShippingPolicy` saga, including timeouts: |
| 20 | +A simple scenario test of a `ShippingPolicy` saga, including timeouts: |
21 | 21 |
|
22 | 22 | snippet: BasicScenarioTest
|
23 | 23 |
|
24 | 24 | ## Creating a testable saga
|
25 | 25 |
|
26 |
| -In many cases a testable saga can be created using only the type parameters from the saga. |
| 26 | +Often, a testable saga can be created using just the type parameters: |
27 | 27 |
|
28 | 28 | snippet: TestableSagaCtor
|
29 | 29 |
|
30 |
| -This assumes that the saga has a parameter-less constructor. If the saga has a constructor that requires services to be injected, a factory can be specified to create an instance of the saga class for each handled message. |
| 30 | +This assumes a parameterless constructor. If the saga requires injected services, a factory can be provided to create an instance per handled message: |
31 | 31 |
|
32 | 32 | snippet: TestableSagaCtorFactory
|
33 | 33 |
|
34 |
| -By default, the `CurrentTime` for the saga is set to `DateTime.UtcNow`. The constructor can also be used to set the `CurrentTime` to a different initial value. For more details see [advancing time](#advancing-time). |
| 34 | +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: |
35 | 35 |
|
36 | 36 | snippet: TestableSagaCtorTime
|
37 | 37 |
|
38 | 38 | ## Handling messages
|
39 | 39 |
|
40 |
| -The testable saga is similar to the saga infrastructure in NServiceBus. Every time it is asked to handle a message, the testable saga instantiates a new instance of the saga class and use the mapping information in the `ConfigureHowToFindSaga` method to locate the correct saga data in the internal storage. |
| 40 | +The testable saga mimics NServiceBus saga infrastructure. Each time it handles a message, it: |
41 | 41 |
|
42 |
| -To have the saga infrastructure handle a message, use the `Handle` method: |
| 42 | +1. Instantiates a new saga instance. |
| 43 | +2. Uses `ConfigureHowToFindSaga` to locate the matching saga data in internal storage. |
| 44 | + |
| 45 | +To handle a message: |
43 | 46 |
|
44 | 47 | snippet: TestableSagaSimpleHandle
|
45 | 48 |
|
46 |
| -If necessary, optional parameters exist to allow the use of a custom `TestableMessageHandlerContext` or specify custom message headers: |
| 49 | +Optional parameters allow using a custom `TestableMessageHandlerContext` or providing custom headers: |
47 | 50 |
|
48 | 51 | snippet: TestableSagaHandleParams
|
49 | 52 |
|
50 | 53 | ## Handler results
|
51 | 54 |
|
52 |
| -The `HandleResult` returned when each message is handled contains information about the message that was handled and the result of that operation which can be used for assertions. |
53 |
| - |
54 |
| -The `HandleResult` class contains: |
| 55 | +Handling a message returns a `HandleResult`, which contains: |
55 | 56 |
|
56 |
| -* `SagaId`: Identifies the `Guid` of the saga that was either created or retrieved from storage. |
57 |
| -* `Completed`: Indicates whether the handler invoked the `MarkAsComplete()` method. |
58 |
| -* `HandledMessage`: Contains the message type, headers, and content of the message that was handled. |
59 |
| -* `Context`: A [`TestableMessageHandlerContext`](/nservicebus/testing/#testing-a-handler) which contains information about messages sent/published as well as any other operations that occurred on the `IMessageHandlerContext` while the message was being handled. |
60 |
| -* `SagaDataSnapshot`: Contains a copy of the saga data after the message handler completed. |
61 |
| -* Convenience methods for finding messages of a given type inside the `Context`: |
| 57 | +* `SagaId`: The `Guid` of the saga instance created or loaded. |
| 58 | +* `Completed`: Whether `MarkAsComplete()` was called. |
| 59 | +* `HandledMessage`: Type, headers, and body of the message handled. |
| 60 | +* `Context`: A [`TestableMessageHandlerContext`](/nservicebus/testing/#testing-a-handler) with sent/published messages and other operations during handling. |
| 61 | +* `SagaDataSnapshot`: Copy of saga data after handling. |
| 62 | +* Helpers for locating specific messages in the `Context`: |
62 | 63 | * `FindSentMessage<TMessage>()`
|
63 | 64 | * `FindPublishedMessage<TMessage>()`
|
64 | 65 | * `FindTimeoutMessage<TMessage>()`
|
65 | 66 | * `FindReplyMessage<TMessage>()`
|
66 | 67 |
|
67 | 68 | ## Advancing time
|
68 | 69 |
|
69 |
| -The testable saga contains a `CurrentTime` property that represents a virtual clock for the saga scenario. The `CurrentTime` property defaults to the time when test execution starts, but can be optionally specified [in the `TestableSaga` constructor](#creating-a-testable-saga). |
| 70 | +`CurrentTime` represents a virtual clock. It defaults to the test start time but can be set in the [constructor](#creating-a-testable-saga). |
70 | 71 |
|
71 |
| -As each message handler runs, timeouts are collected in an internal timeout storage. By calling the `AdvanceTime` method, these timeouts will come due and the messages they contain will be handled. The `AdvanceTime` method returns an array of `HandleResult`, one for each timeout that is handled. |
| 72 | +Timeouts are stored during message handling. Calling `AdvanceTime` processes due timeouts and returns an array of `HandleResult`: |
72 | 73 |
|
73 | 74 | snippet: TestableSagaAdvanceTime
|
74 | 75 |
|
75 |
| -If a custom `TestableMessageHandlerContext` is needed to process each timeout, an optional parameter allows creating them: |
| 76 | +Need custom `TestableMessageHandlerContext` per timeout? Use the overload: |
76 | 77 |
|
77 | 78 | snippet: TestableSagaAdvanceTimeParams
|
78 | 79 |
|
79 | 80 | ## Simulating external handlers
|
80 | 81 |
|
81 |
| -Many sagas send commands to external handlers, which do some work, then send a reply message back to the saga so that the saga can move on to the next step of a multi-step process. These reply messages are [auto-correlated](/nservicebus/sagas/message-correlation.md#auto-correlation): the saga includes the saga ID as a message header in the outbound message, and the external handler returns that message when it does a reply. |
| 82 | +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. |
82 | 83 |
|
83 |
| -In a saga scenario test, the external handler's response can be simulated using the `SimulateReply` method: |
| 84 | +Simulate such replies with `SimulateReply`: |
84 | 85 |
|
85 | 86 | snippet: TestableSagaSimulateReply
|
86 | 87 |
|
87 |
| -When the saga being tested sends a `DoStep1` command, the reply is simulated using the provided `Func<TSagaMessage, TReplyMessage>` delegate. The resulting `Step1Response` message is added to the testable saga's [internal queue](#queued-messages), including the header containing the saga's ID so that a `ConfigureHowToFindSaga` mapping is not required. |
88 |
| - |
89 |
| -Alternatively, a reply message can be handled directly without using a simulator, but then the `SagaId` value must be provided: |
| 88 | +The reply is enqueued internally, with the correlation header set. If you want to manually handle a reply instead: |
90 | 89 |
|
91 | 90 | snippet: TestableSagaHandleReply
|
92 | 91 |
|
93 |
| -The `HandleReply` method also contains optional parameters for a custom `TestableMessageHandlerContext` or additional message headers: |
| 92 | +You can also supply custom headers or a custom context: |
94 | 93 |
|
95 | 94 | snippet: TestableSagaHandleReplyParams
|
96 | 95 |
|
97 | 96 | ## Queued messages
|
98 | 97 |
|
99 |
| -Any message generated of a type that is handled by the saga is added to the testable saga's internal queue. This includes: |
| 98 | +Any message sent/published that the saga handles is queued internally. This includes: |
100 | 99 |
|
101 |
| -* When a saga handler sends or publishes any message that the saga itself handles. This is commonly done within a saga to create a new transactional scope around a new message. |
102 |
| -* When using a [external handler simulator](#simulating-external-handlers), the resulting message is added to the queue. |
| 100 | +* Saga sends/publishes of messages it also handles. |
| 101 | +* Messages produced by [external handler simulations](#simulating-external-handlers). |
103 | 102 |
|
104 |
| -The testable saga has several methods available to evaluate what is in the queue, which can be used for test assertions |
| 103 | +You can inspect and assert against the message queue: |
105 | 104 |
|
106 | 105 | snippet: TestableSagaQueueOperations
|
107 | 106 |
|
108 |
| -The next message in the queue is handled by calling `HandleQueuedMessage()`: |
| 107 | +To process the next queued message: |
109 | 108 |
|
110 | 109 | snippet: TestableSagaHandleQueuedMessage
|
111 | 110 |
|
112 | 111 | > [!NOTE]
|
113 |
| -> Using `HandleQueuedMessage()` allows specific ordering of message processing in order to write tests related to specific ordering and race condition concerns. Whether or not a timeout or a reply message is handled first in a specific scenario is controlled by whether the test calls the `AdvanceTime()` or `HandleQueuedMessage()` method. |
| 112 | +> 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. |
114 | 113 |
|
115 | 114 | ## Additional examples
|
116 | 115 |
|
117 |
| -For more examples of what is possible with saga scenario tests, see the [saga tests in the NServiceBus.Testing repository](https://github.com/Particular/NServiceBus.Testing/tree/master/src/NServiceBus.Testing.Tests/Sagas). |
| 116 | +For more usage patterns, see the [NServiceBus.Testing saga tests](https://github.com/Particular/NServiceBus.Testing/tree/master/src/NServiceBus.Testing.Tests/Sagas). |
0 commit comments