Testing NServiceBus

Project Hosting: https://github.com/Particular/NServiceBus.Testing
Nuget Package: NServiceBus.Testing (Version: 5.x)
Target NServiceBus Version: 5.x

Testing enterprise-scale distributed systems is a challenge. A dedicated NuGet package, NServiceBus.Testing, is provided with tools that allow 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.

Structure

The tests have to follow a well-defined structure, otherwise a subtle issues might arise at runtime. The recommended structure is presented in the code snippets in this article.

Before executing any test method, it is necessary to call the Test.Initialize() method (or any of its overloads).

The tests are written using fluent API, but expectations should be generally specified before invoking the tested behaviour (the exception is testing saga timeouts).

If unobtrusive mode is used, the conventions must be configured in the Test.Initialize() method:

Test.Initialize(
    customisations: config =>
    {
        var conventions = config.Conventions();
        conventions.DefiningMessagesAs(MyMessageTypeConvention);
    });

Handlers

Testing handlers focuses on their externally visible behavior - the types of messages they send or reply with.

[TestFixture]
public class Tests
{
    [Test]
    public void Run()
    {
        Test.Initialize();

        Test.Handler<MyHandler>()
            .ExpectReply<ResponseMessage>(
                check: message =>
                {
                    return message.String == "hello";
                })
            .OnMessage<RequestMessage>(
                initializeMessage: message =>
                {
                    message.String = "hello";
                });
    }
}

public class MyHandler :
    IHandleMessages<RequestMessage>
{
    public IBus Bus { get; set; }

    public void Handle(RequestMessage message)
    {
        var reply = new ResponseMessage
        {
            String = message.String
        };
        Bus.Reply(reply);
    }
}

The test verifies that when a message of the type RequestMessage is processed by MyHandler, it responds with a message of the type ResponseMessage. Also, the test checks that if the request message's String property value is "hello" then that should be the value of the String property of the response message.

Sagas

Testing sagas focuses on their externally visible behavior - the types of messages they send or reply with, but it's also possible to verify that saga requested a timeout or was completed.

[Test]
public void Run()
{
    Test.Initialize();
    Test.Saga<MySaga>()
        .ExpectReplyToOriginator<MyResponse>()
        .ExpectTimeoutToBeSetIn<StartsSaga>(
            check: (state, span) =>
            {
                return span == TimeSpan.FromDays(7);
            })
        .ExpectPublish<MyEvent>()
        .ExpectSend<MyCommand>()
        .When(
            sagaIsInvoked: saga =>
            {
                saga.Handle(new StartsSaga());
            })
        .WhenSagaTimesOut()
        .ExpectPublish<MyOtherEvent>()
        .AssertSagaCompletionIs(true);
}

The test verifies that when a message of the type StartsSaga is processed by MySaga, the saga replies to the sender with the MyResponse message, publishes MyEvent, sends MyCommand and requests a timeout for message StartsSaga. Also it checks if the saga publishes MyOtherEvent and is completed, after the timeout expires.

Note that the expectation for MyOtherEvent is set only after the message is sent.

Interface messages

To support testing of interface messages use .WhenHandling<T>() method, where T is the interface type.

Header manipulation

Message handlers retrieve information from the incoming message headers and set headers for the outgoing messages, for example NServiceBus uses that set correlation Id or address for reply. Headers can be also used for passing custom information.

[TestFixture]
public class Tests
{
    [Test]
    public void Run()
    {
        Test.Initialize();

        Test.Handler<MyMessageHandler>()
            .SetIncomingHeader("MyHeaderKey", "myHeaderValue")
            .ExpectReply<ResponseMessage>(
                check: message =>
                {
                    return Test.Bus.GetMessageHeader(message, "MyHeaderKey") == "myHeaderValue";
                })
            .OnMessage<RequestMessage>(
                initializeMessage: message =>
                {
                    message.String = "hello";
                });
    }
}

class MyMessageHandler :
    IHandleMessages<RequestMessage>
{
    public IBus Bus { get; set; }

    public void Handle(RequestMessage message)
    {
        var header = Bus.GetMessageHeader(message, "MyHeaderKey");

        var responseMessage = new ResponseMessage();
        Bus.SetMessageHeader(responseMessage, "MyHeaderKey", header);
        Bus.Reply(responseMessage);
    }
}

This test asserts that the value of the outgoing header has been set.

Injecting additional dependencies into the service layer

Many of the message handling classes in the service layer make use of other objects to perform their work. When testing these classes, replace those objects with "stubs" so that the class under test is isolated.

[TestFixture]
public class Tests
{
    [Test]
    public void RunWithDependencyInjection()
    {
        Test.Initialize();

        var mockService = new MyService();
        Test.Handler(
            handlerCreationCallback: bus =>
            {
                return new WithDependencyInjectionHandler(mockService);
            });
        // Rest of test
    }
}

class WithDependencyInjectionHandler :
    IHandleMessages<MyMessage>
{
    MyService myService;

    public WithDependencyInjectionHandler(MyService myService)
    {
        this.myService = myService;
    }

    public void Handle(MyMessage message)
    {
    }
}

Injecting the IBus

In order to access the IBus interface from the tested handler, use constructor injection.

[TestFixture]
public class Tests2
{
    [Test]
    public void RunWithConstructorInjectedBus()
    {
        Test.Initialize();

        var mockService = new MyService();
        Test.Handler(
            handlerCreationCallback: bus =>
            {
                return new WithConstructorInjectedBusHandler(bus);
            });
        // Rest of test
    }
}

class WithConstructorInjectedBusHandler :
    IHandleMessages<MyMessage>
{
    IBus bus;

    public WithConstructorInjectedBusHandler(IBus bus)
    {
        this.bus = bus;
    }

    public void Handle(MyMessage message)
    {
    }
}

Limiting scanning

To limit the assemblies and types scanned it is possible to use the Initialize() overload that accepts a delegate to customize the ConfigurationBuilder. The list of assemblies scanned must include NServiceBus.Testing.dll

var assembliesToScan = new List<Assembly>
{
    typeof(HandlerToTest).Assembly,
    Assembly.LoadFrom("NServiceBus.Testing.dll")
};
Test.Initialize(
    customisations: configuration =>
    {
        configuration.AssembliesToScan(assembliesToScan);
    });

Samples


Last modified