This is part of the NServiceBus Upgrade Guide from Version 7 to 8, which also includes the following individual upgrade guides for specific components:
Feature Details
- Upgrading the data bus from version 7 to 8
- Dependency Injection changes
- Upgrade NServiceBus downstreams from Version 7 to 8
- Upgrading message contracts from Version 7 to 8
- Upgrade NServiceBus pipeline extensions from Version 7 to 8
- Transport configuration changes
Transports
- AmazonSQS Transport Upgrade Version 5 to 6
- Azure Service Bus Transport Upgrade Version 2 to 3
- Azure Storage Queues Transport Upgrade Version 10 to 11
- MSMQ Transport Upgrade Version 1 to 2
- MSMQ Transport Upgrade Version 2 to 2.0.4
- RabbitMQ Transport Upgrade Version 7 to 8
- SQL Server Transport Upgrade Version 6 to 7
Persistence
- Cosmos DB Persistence Upgrade from 1 to 2
- NHibernate Persistence Upgrade Version 8 to 9
- RavenDB Persistence Upgrade from 7 to 8
- SQL Persistence Upgrade Version 6 to 7
Hosting
Other
Fluent-style API is not supported
Starting with NServiceBus version 8, the fluent-style testing API is not supported. Tests should be written in an Arrange-Act-Assert (AAA) style. Tests written this way will create the handler or saga to be tested, and call methods on them directly, passing in a testable message handler context that will capture outgoing operations that can be asserted on afterwards.
Testing a handler
To test a handler, create it with the necessary dependencies, then call the Handle
method directly. Pass in an instance of TestableMessageHandlerContext
which will collect all outgoing operations. This context allows customization of metadata about the incoming message, including headers.
An example of the same test written in both the Arrange-Act-Assert style and the fluent style:
8.x NServiceBus.Testing
[TestFixture]
class ArrangeActAssertHandlerTesting
{
[Test]
public async Task TestHandler()
{
// Arrange
var handler = new RequestMessageHandler();
// Act
var requestMessage = new RequestMessage {String = "hello"};
var messageContext = new TestableMessageHandlerContext();
await handler.Handle(requestMessage, messageContext);
// Assert
Assert.That(messageContext.RepliedMessages.Any(x =>
x.Message<ResponseMessage>()?.String == "hello"),
Is.True,
"Should send a ResponseMessage reply that echoes the provided string");
}
}
7.x - 7.3 NServiceBus.Testing
[TestFixture]
class FluentHandlerTesting
{
[Test]
public async Task TestHandler()
{
await Test.Handler<RequestMessageHandler>() // Arrange
.ExpectReply<ResponseMessage>( // Assert
check: message =>
{
return message.String == "hello";
})
.OnMessageAsync<RequestMessage>( // Act
initializeMessage: message =>
{
message.String = "hello";
});
}
}
See the handler unit testing documentation for more information.
Testing a saga
To test a saga, use the saga scenario testing framework. The TestableSaga
class emulates a saga that can receive multiple messages, stores saga data in between messages, and can virtually advance time to observe the processing of timeouts. Assertions can be made on the result of any message being processed by the testable saga.
For each operation, a custom TestableMessageHandlerContext
can be supplied, but if not provided one will be generated by the testing framework to limit unnecessary boilerplate.
This is an example showing two state changes. The first starts the saga that triggers a timeout, publishes an event, and sends a message. The second state change happens when the timeout occurs, causing another event to be published, and the saga to be completed.
8.x NServiceBus.Testing
[TestFixture]
class ArrangeActAssertSagaTests
{
[Test]
public async Task When_Saga_is_started_by_StartsSaga()
{
// Arrange
var testableSaga = new TestableSaga<MySaga, MySaga.SagaData>();
// Act
var message = new StartsSaga { MyId = "some-id" };
var startResult = await testableSaga.Handle(message);
Assert.Multiple(() =>
{
// Assert
Assert.That(startResult.FindPublishedMessage<MyEvent>(), Is.Not.Null,
"MyEvent should be published");
Assert.That(startResult.FindSentMessage<MyCommand>(), Is.Not.Null,
"MyCommand should be sent");
});
// Instead of asserting on timeouts placed, virtually advance time
// and then assert on the results
var advanceTimeResults = await testableSaga.AdvanceTime(TimeSpan.FromDays(7));
Assert.That(advanceTimeResults, Has.Length.EqualTo(1));
var timeoutResult = advanceTimeResults.Single();
Assert.That(timeoutResult.Completed, Is.True);
}
}
7.x - 7.3 NServiceBus.Testing
[TestFixture]
class FluentSagaTests
{
[Test]
public void TestSagaFluent()
{
Test.Saga<MySaga>()
.ExpectTimeoutToBeSetIn<StartsSaga>(
check: (state, span) =>
{
return span == TimeSpan.FromDays(7);
})
.ExpectPublish<MyEvent>()
.ExpectSend<MyCommand>()
.When(
sagaIsInvoked: (saga, context) =>
{
return saga.Handle(new StartsSaga(), context);
})
.ExpectPublish<MyOtherEvent>()
.WhenSagaTimesOut()
.ExpectSagaCompleted();
}
}
Fluent-style saga tests will often include multiple state changes. Arrange-Act-Assert (AAA) tests normally test a single state change in isolation, however this is difficult with sagas because too many internal details of how NServiceBus sagas work must be encoded into the test, limiting their effectiveness. For this reason the scenario test pattern is recommended.
It is also possible to test sagas in isolation in a manner more similar to handlers.
Uniform Session
The NServiceBus.
package provided the WithUniformSession
method to configure fluent-style tests to work with IUniformSession
. With AAA-style tests, a new instance of the TestableUniformSession
class can be created and passed to any class with a dependency on IUniformSession
:
[Test]
public async Task TestWithUniformSession()
{
var uniformSession = new TestableUniformSession();
var component = new SharedComponent(uniformSession);
await component.DoSomething();
Assert.That(uniformSession.PublishedMessages, Has.Length.EqualTo(1));
}
For scenarios where the tested code path both invokes operations of IUniformSession
and a pipeline context like IMessageHandlerContext
, the TestableUniformSession
can be configured to wrap the context:
8.x NServiceBus.Testing
[Test]
public async Task TestWithMessageHandler()
{
var handlerContext = new TestableMessageHandlerContext();
var uniformSession = new TestableUniformSession(handlerContext);
var sharedComponent = new SharedComponent(uniformSession);
var messageHandler = new SomeMessageHandler(sharedComponent);
// message handler calls SharedComponent within Handle
await messageHandler.Handle(new SomeEvent(), handlerContext);
Assert.Multiple(() =>
{
Assert.That(uniformSession.SentMessages, Has.Length.EqualTo(1));
// the message handler context and the uniform session share the same state, so these assertions are identical
Assert.That(handlerContext.SentMessages, Has.Length.EqualTo(1));
});
}
7.x - 7.3 NServiceBus.Testing
[Test]
public void TestWithMessageHandler()
{
var uniformSession = new TestableUniformSession();
var sharedComponent = new SharedComponent(uniformSession);
var handler = new SomeMessageHandler(sharedComponent);
Test.Handler(handler)
.WithUniformSession(uniformSession)
.ExpectPublish<SomeEvent>()
.OnMessage<SomeMessage>();
}
This approach also works for code that use IUniformSession
and IMessageSession
in the same code path:
[Test]
public async Task TestWithMessageSession()
{
var messageSession = new TestableMessageSession();
var uniformSession = new TestableUniformSession(messageSession);
var sharedComponent = new SharedComponent(uniformSession);
var myService = new MyService(sharedComponent);
// MyService calls SharedComponent within Start
await myService.Start(messageSession);
Assert.Multiple(() =>
{
Assert.That(uniformSession.SentMessages, Has.Length.EqualTo(1));
// the message session and the uniform session share the same state, so these assertions are identical
Assert.That(messageSession.SentMessages, Has.Length.EqualTo(1));
});
}
TestableBehaviorContext
To add delivery constraints to the TestableBehaviorContext
class, use DispatchProperties
instead of AddDeliveryConstraint
on the context bag.
public void UsingDispatchPropertiesInTestableBehaviorContext(NServiceBus.Testing.TestableBehaviorContext context)
{
context.Extensions.Set(new DispatchProperties
{
DelayDeliveryWith = new DelayDeliveryWith(TimeSpan.FromDays(1))
});
}