NServiceBus Testing Upgrade Version 7 to 8

Component: Testing
This page targets a pre-release version. Pre-releases are subject to change and samples are not guaranteed to be fully functional.

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-pre 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, create it with the necessary dependencies (including the Data property), 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.

Fluent-style saga tests will often include multiple state changes. Arrange-Act-Assert (AAA) tests should test a single state change in isolation. The state of the saga can be configured manually before each test as part of the Arrange step.

This is an example showing two state changes. The first starts the saga that triggers a timeout, then sends a reply, 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. These can be split into two tests (one for each state change) when using the Arrange-Act-Assert style.

8-pre NServiceBus.Testing
[TestFixture]
class ArrangeActAssertSagaTests
{
    [Test]
    public async Task When_Saga_is_started_by_StartsSaga()
    {
        // Arrange
        var saga = new MySaga
        {
            Data = new MySaga.SagaData
            {
                Originator = "Originator"
            }
        };

        // Act
        var message = new StartsSaga();
        var messageHandlerContext = new TestableMessageHandlerContext();

        await saga.Handle(message, messageHandlerContext);

        // Assert
        Assert.IsTrue(messageHandlerContext.RepliedMessages.Any(x =>
            x.Message is MyResponse &&
            x.Options.GetDestination() == "Originator"),
            "A MyResponse reply should be sent to the originator"
        );

        Assert.IsTrue(messageHandlerContext.TimeoutMessages.Any(x =>
            x.Message is StartsSaga &&
            x.Within == TimeSpan.FromDays(7)),
            "The StartsSaga message should be deferred for 7 days"
        );

        Assert.IsTrue(messageHandlerContext.PublishedMessages.Any(x =>
            x.Message is MyEvent),
            "MyEvent should be published"
        );

        Assert.IsTrue(messageHandlerContext.SentMessages.Any(x =>
            x.Message is MyCommand),
            "MyCommand should be sent"
        );
    }

    [Test]
    public async Task When_StartsSaga_Timeout_completes()
    {
        // Arrange
        var saga = new MySaga
        {
            Data = new MySaga.SagaData()
        };

        // Act
        var timeoutMessage = new StartsSaga();
        var timeoutHandlerContext = new TestableMessageHandlerContext();
        await saga.Timeout(timeoutMessage, timeoutHandlerContext);

        // Assert
        Assert.IsTrue(timeoutHandlerContext.PublishedMessages.Any(x =>
            x.Message is MyOtherEvent),
            "MyOtherEvent should be published"
        );

        Assert.IsTrue(saga.Completed, "Saga should be completed");
    }
}
7.x - 7.3 NServiceBus.Testing
[TestFixture]
class FluentSagaTests
{
    [Test]
    public void TestSagaFluent()
    {
        Test.Saga<MySaga>()
            .ExpectReplyToOriginator<MyResponse>()
            .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();
    }
}

See the saga unit testing documentation for more information.

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-pre 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


Last modified