title | summary | reviewed | component | related | |
---|---|---|---|---|---|
Testing NServiceBus |
Develop service layers and long-running processes using test-driven development. |
2023-06-19 |
Testing |
|
Testing enterprise-scale distributed systems is a challenge. A dedicated NuGet package, NServiceBus.Testing
, is provided with tools for unit testing endpoint handlers and sagas.
The testing package can be used with any .NET unit testing framework, such as NUnit, xUnit.net or MSTest.
Testing a message handler is done using the TestableMessageHandlerContext
class provided by the NServiceBus.Testing
package. This class implements IMessageHandlerContext
and can be passed to the handler under test. After the handler is executed, the TestableMessageHandlerContext
can be interrogated to assert that various actions (sending a message, publishing an event, etc.) occurred in the handler as expected.
Property | Description |
---|---|
SentMessages |
A list of all messages sent by context.Send() |
PublishedMessages |
A list of all messages published by context.Publish() |
RepliedMessages |
A list of all messages sent using context.Reply() |
TimeoutMessages |
A list of all messages resulting from use of Saga.RequestTimeout() |
ForwardedMessages |
A list of all forwarding destinations set by context.ForwardCurrentMessageTo() |
MessageHeaders |
Gets the list of key/value pairs found in the header of the message |
HandlerInvocationAborted |
Indicates if DoNotContinueDispatchingCurrentMessageToHandlers() was called |
Given the following handler:
snippet: SimpleHandler
This test verifies that a Reply
occurred:
snippet: HandlerTest
When using interface messages, an instance of the message can be created by either defining a custom implementation of the message interface or by using the MessageMapper
as shown in the following snippet:
snippet: InterfaceMessageCreation
Use TestableMessageSession
to test message operations outside of handlers. The following properties are available:
Property | Description |
---|---|
SentMessages |
A list of all messages sent by session.Send() |
PublishedMessages |
A list of all messages published by session.Publish() |
Subscriptions |
A list of all message types explicitly subscribed to using session.Subscribe() |
Unsubscriptions |
A list of all message types explicitly unsubscribed to using session.Unsubscribe() |
The following code shows how to verify that a message was Sent
using IMessageSession
.
snippet: TestMessageSessionSend
Testing a saga can be accomplished in one of two ways:
- Test saga handler methods individually, similar to testing a handler. With this testing method, the test must supply some of the conditions (such as the contents of the saga data) normally provided by the NServiceBus framework. This testing method is described below.
- Saga scenario testing, in which the results of a scenario consisting of multiple message inputs can be tested with a virtual saga storage to store the saga data between messages. This also adds a virtual concept of time, allowing the test to advance time by a set period between messages and enabling observation of the results of the saga timeouts that would be fired during that time. With this testing method, the testing framework emulates much of the behavior provided by NServiceBus in production.
Testing a saga uses the same TestableMessageHandlerContext
as testing a handler. The same properties are used to perform assertions after a saga method is invoked.
Note
Because timeouts are technically sent messages, any timeout requested from the saga will appear in both the TimeoutMessages
and SentMessages
collections of the TestableMessageHandlerContext
.
Here's an example of a saga, that processes an order and gives a 10% discount for orders above an amount of 1000:
snippet: SampleSaga
The following unit test checks that the total amount has the discount applied:
snippet: SagaTest
Message pipeline behaviors also can be tested, but using different testable context objects. Each of the pipeline stages uses a specific interface for its context, and each context interface has a testable implementation.
To determine the testable context for a behavior context, replace the I
at the beginning of the interface name with Testable
.
For example:
- A behavior using
IIncomingLogicalMessageContext
can be tested usingTestableIncomingLogicalMessageContext
. - A behavior using
IInvokeHandlerContext
can be tested usingTestableInvokeHandlerContext
.
Refer to the pipeline stages document for a complete list of the available behavior contexts.
Each of these testable types contains properties similar to those found in TestableMessageHandlerContext
that can be used to assert that a behavior is working as designed.
The following custom behavior adds a header to an outgoing message in case the message is of the type MyResponse
:
snippet: SampleBehavior
The behavior can be tested similar to a message handler or a saga by using a testable representation of the context:
snippet: BehaviorTest
To test that logging is performed correctly, use the TestingLoggerFactory
. The factory writes to a StringWriter
to allow unit tests to assert on log statements.
Note
Using WriteTo
or Level
set the provided parameters to the statically cached factory for the lifetime of the application domain. For isolation of logging in concurrent scenarios it is recommended to use BeginScope
that was introduced in Version 7.2.
The following code show how to verify that logging is performed by the message handler.
snippet: LoggerTestingSetup
The setup fixture above sets the testing logging factory once per assembly because the factory is statically cached during the lifetime of the application domain. Subsequent test executions then clear the logged statements before every test run as shown below.
snippet: LoggerTesting
partial: loggerscope