Testing NServiceBus

Source
NuGet Package NServiceBus.Testing (6.x)
Target NServiceBus Version: 6.x

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 handler

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.

PropertyDescription
SentMessagesA list of all messages sent by context.Send()
PublishedMessagesA list of all messages published by context.Publish()
RepliedMessagesA list of all messages sent using context.Reply()
TimeoutMessagesA list of all messages resulting from use of Saga.RequestTimeout()
ForwardedMessagesA list of all forwarding destinations set by context.ForwardCurrentMessageTo()
MessageHeadersGets the list of key/value pairs found in the header of the message
HandlerInvocationAbortedIndicates if DoNotContinueDispatchingCurrentMessageToHandlers() was called

Example

Given the following handler:

public class MyReplyingHandler :
    IHandleMessages<MyRequest>
{
    public Task Handle(MyRequest message, IMessageHandlerContext context)
    {
        return context.Reply(new MyResponse());
    }
}

This test verifies that a Reply occurred:

[Test]
public async Task ShouldReplyWithResponseMessage()
{
    var handler = new MyReplyingHandler();
    var context = new TestableMessageHandlerContext();

    await handler.Handle(new MyRequest(), context)
        .ConfigureAwait(false);

    Assert.AreEqual(1, context.RepliedMessages.Length);
    Assert.IsInstanceOf<MyResponse>(context.RepliedMessages[0].Message);
}

Testing message session operations

Use TestableMessageSession to test message operations outside of handlers. The following properties are available:

PropertyDescription
SentMessagesA list of all messages sent by session.Send()
PublishedMessagesA list of all messages published by session.Publish()
SubscriptionsA list of all message types explicitly subscribed to using session.Subscribe()
UnsubscriptionsA list of all message types explicitly unsubscribed to using session.Unsubscribe()

Example

The following code shows how to verify that a message was Sent using IMessageSession.

[Test]
public async Task ShouldLogCorrectly()
{
    var testableSession = new TestableMessageSession();

    var somethingThatUsesTheMessageSession = new SomethingThatUsesTheMessageSession(testableSession);

    await somethingThatUsesTheMessageSession.DoSomething();

    Assert.AreEqual(1, testableSession.SentMessages.Length);
    Assert.IsInstanceOf<MyResponse>(testableSession.SentMessages[0].Message);
}

Testing a saga

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.

Because timeouts are technically sent messages, any timeout requested from the saga will appear in both the TimeoutMessages and SentMessages collections of the TestableMessageHandlerContext.

Example

Here's an example of a saga, that processes an order and gives a 10% discount for orders above an amount of 1000:

public class DiscountPolicy :
    Saga<DiscountPolicyData>,
    IAmStartedByMessages<SubmitOrder>
{
    public Task Handle(SubmitOrder message, IMessageHandlerContext context)
    {
        Data.CustomerId = message.CustomerId;
        Data.TotalAmount += message.TotalAmount;

        if (Data.TotalAmount >= 1000)
        {
            return ProcessWithDiscount(message, context);
        }
        return ProcessOrder(message, context);
    }

    Task ProcessWithDiscount(SubmitOrder message, IMessageHandlerContext context)
    {
        var processOrder = new ProcessOrder
        {
            CustomerId = Data.CustomerId,
            OrderId = message.OrderId,
            TotalAmount = message.TotalAmount * (decimal)0.9
        };
        return context.Send(processOrder);
    }

    Task ProcessOrder(SubmitOrder message, IMessageHandlerContext context)
    {
        var processOrder = new ProcessOrder
        {
            CustomerId = Data.CustomerId,
            OrderId = message.OrderId,
            TotalAmount = message.TotalAmount
        };
        return context.Send(processOrder);
    }

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<DiscountPolicyData> mapper)
    {
    }
}

The following unit test checks that the total amount has the discount applied:

[Test]
public async Task ShouldProcessDiscountOrder()
{
    var saga = new DiscountPolicy
    {
        Data = new DiscountPolicyData()
    };
    var context = new TestableMessageHandlerContext();

    var discountOrder = new SubmitOrder
    {
        CustomerId = Guid.NewGuid(),
        OrderId = Guid.NewGuid(),
        TotalAmount = 1000
    };

    await saga.Handle(discountOrder, context)
        .ConfigureAwait(false);

    var processMessage = (ProcessOrder)context.SentMessages[0].Message;
    Assert.AreEqual(900, processMessage.TotalAmount);
}

Testing a behavior

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 using TestableIncomingLogicalMessageContext.
  • A behavior using IInvokeHandlerContext can be tested using TestableInvokeHandlerContext.

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.

Example

The following custom behavior adds a header to an outgoing message in case the message is of the type MyResponse:

public class CustomBehavior :
    Behavior<IOutgoingLogicalMessageContext>
{
    public override Task Invoke(IOutgoingLogicalMessageContext context, Func<Task> next)
    {
        if (context.Message.MessageType == typeof(MyResponse))
        {
            context.Headers.Add("custom-header", "custom header value");
        }

        return next();
    }
}

The behavior can be tested similar to a message handler or a saga by using a testable representation of the context:

[Test]
public async Task ShouldAddCustomHeaderToMyResponse()
{
    var behavior = new CustomBehavior();
    var context = new TestableOutgoingLogicalMessageContext
    {
        Message = new OutgoingLogicalMessage(typeof(MyResponse), new MyResponse())
    };

    await behavior.Invoke(context, () => Task.CompletedTask)
        .ConfigureAwait(false);

    Assert.AreEqual("custom header value", context.Headers["custom-header"]);
}

Testing logging behavior

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.

Example

The following code show how to verify that logging is performed by the message handler.

[Test]
public async Task ShouldLogCorrectly()
{
    var logStatements = new StringBuilder();

    LogManager.Use<TestingLoggerFactory>()
        .WriteTo(new StringWriter(logStatements));

    var handler = new MyHandlerWithLogging();

    await handler.Handle(new MyRequest(), new TestableMessageHandlerContext())
        .ConfigureAwait(false);

    StringAssert.Contains("Some log message", logStatements.ToString());
}

Samples

Related Articles


Last modified