Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

NServiceBus Testing Upgrade Version 7 to 8

Component: Testing

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.IsTrue(messageContext.RepliedMessages.Any(x =>
            x.Message<ResponseMessage>()?.String == "hello"),
            "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
        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.Length, Is.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.UniformSession.Testing 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.AreEqual(1, uniformSession.PublishedMessages.Length);
}

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.AreEqual(1, uniformSession.SentMessages.Length);
    // the message handler context and the uniform session share the same state, so these assertions are identical
    Assert.AreEqual(1, handlerContext.SentMessages.Length);
}
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.AreEqual(1, uniformSession.SentMessages.Length);
    // the message session and the uniform session share the same state, so these assertions are identical
    Assert.AreEqual(1, messageSession.SentMessages.Length);
}

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))
    });
}

Related Articles