Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

Testing NServiceBus

NuGet Package: NServiceBus.Testing (8.x)
Target Version: NServiceBus 8.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);

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

Interface messages

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:

var messageMapper = new MessageMapper();
var myMessage = messageMapper.CreateInstance<IMyMessage>(message => { /* ... */ });

await handler.Handle(myMessage, context);

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 can be accomplished in one of two ways:

  1. 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.
  2. 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 saga handlers

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)
    {
        mapper.MapSaga(saga => saga.CustomerId)
            .ToMessage<SubmitOrder>(msg => msg.CustomerId);
    }
}

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

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

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

    // act
    await saga.Handle(discountOrder, context);

    // assert
    var processMessage = (ProcessOrder)context.SentMessages[0].Message;
    Assert.That(processMessage.TotalAmount, Is.EqualTo(900));
    Assert.That(saga.Completed, Is.False);
}

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

    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

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.

[SetUpFixture]
public class LoggingSetupFixture
{
    static StringBuilder logStatements = new StringBuilder();

    [OneTimeSetUp]
    public void SetUp()
    {
        LogManager.Use<TestingLoggerFactory>()
            .WriteTo(new StringWriter(logStatements));
    }

    public static string LogStatements => logStatements.ToString();

    public static void Clear()
    {
        logStatements.Clear();
    }
}

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.

[SetUp]
public void SetUp()
{
    LoggingSetupFixture.Clear();
}

[Test]
public async Task ShouldLogCorrectly()
{
    var handler = new MyHandlerWithLogging();

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

    StringAssert.Contains("Some log message", LoggingSetupFixture.LogStatements);
}

Starting from Version 7.2 and above a scope can be defined for a user defined scope. Within that scope all the log statements will be collected on the text writer available within that scope.

[TestFixture]
public class LoggingTestsAmbient
{
    StringBuilder logStatements;
    IDisposable scope;

    [SetUp]
    public void SetUp()
    {
        logStatements = new StringBuilder();

        scope = LogManager.Use<TestingLoggerFactory>()
            .BeginScope(new StringWriter(logStatements));
    }

    [TearDown]
    public void Teardown()
    {
        scope.Dispose();
    }

    [Test]
    public async Task ShouldLogCorrectly()
    {
        var handler = new MyHandlerWithLogging();

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

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

Samples

Related Articles


Last modified