Skip to content

Commit e50598a

Browse files
authored
Readability (#7168)
1 parent 002dea7 commit e50598a

File tree

1 file changed

+36
-37
lines changed

1 file changed

+36
-37
lines changed
Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,116 @@
11
---
22
title: Saga scenario testing
33
summary: Develop service layers and long-running processes using test-driven development.
4-
reviewed: 2023-06-19
4+
reviewed: 2025-05-30
55
component: Testing
66
versions: '[7.4,)'
77
related:
88
---
99

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.
1111

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:
1313

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.
1616
* Stores timeouts internally, which can be triggered by [advancing time](#advancing-time).
1717

1818
## Example
1919

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:
2121

2222
snippet: BasicScenarioTest
2323

2424
## Creating a testable saga
2525

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:
2727

2828
snippet: TestableSagaCtor
2929

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:
3131

3232
snippet: TestableSagaCtorFactory
3333

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:
3535

3636
snippet: TestableSagaCtorTime
3737

3838
## Handling messages
3939

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:
4141

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:
4346

4447
snippet: TestableSagaSimpleHandle
4548

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:
4750

4851
snippet: TestableSagaHandleParams
4952

5053
## Handler results
5154

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:
5556

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`:
6263
* `FindSentMessage<TMessage>()`
6364
* `FindPublishedMessage<TMessage>()`
6465
* `FindTimeoutMessage<TMessage>()`
6566
* `FindReplyMessage<TMessage>()`
6667

6768
## Advancing time
6869

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).
7071

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`:
7273

7374
snippet: TestableSagaAdvanceTime
7475

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:
7677

7778
snippet: TestableSagaAdvanceTimeParams
7879

7980
## Simulating external handlers
8081

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.
8283

83-
In a saga scenario test, the external handler's response can be simulated using the `SimulateReply` method:
84+
Simulate such replies with `SimulateReply`:
8485

8586
snippet: TestableSagaSimulateReply
8687

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:
9089

9190
snippet: TestableSagaHandleReply
9291

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:
9493

9594
snippet: TestableSagaHandleReplyParams
9695

9796
## Queued messages
9897

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:
10099

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).
103102

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:
105104

106105
snippet: TestableSagaQueueOperations
107106

108-
The next message in the queue is handled by calling `HandleQueuedMessage()`:
107+
To process the next queued message:
109108

110109
snippet: TestableSagaHandleQueuedMessage
111110

112111
> [!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.
114113
115114
## Additional examples
116115

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

Comments
 (0)