Testing NServiceBus

Developing enterprise-scale distributed systems is hard and testing them is just as challenging a task. The architectural approach supported by NServiceBus makes these challenges more manageable. The testing facilities provided make unit testing endpoints and workflows easy, allowing developing service layers and long-running processes using Test-Driven Development.

Getting started

NServiceBus ships with a stand alone testing helper NuGet package that makes testing a lot simpler.

To install this package:

Install-Package NServiceBus.Testing

Once the package is installed, create a new test using any of the testing frameworks, such as NUnit, xUnit.net or MSBuild.

In Versions 5 and below, Test.Initialize() (or any of its overloads) must be called before executing any test method.

To limit the assemblies and types scanned by the test framework 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

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

Test structure

The NServiceBus testing framework in all versions offers a fluent API for defining tests as shown in the snippets below.

Note that tests written using fluent API follow a specific structure, that is they specify expectations before invoking the tested behavior. Violating this order might in some cases lead to subtle issues when executing tests. The recommended approach is to follow the order presented in code snippets.

In NServiceBus Version 6 and above it's also possible to write tests without using fluent API, in the traditional Arrange-Act-Assert (AAA) fashion. For more details refer to the Unit Testing NServiceBus 6 sample.

Testing the service layer

The service layer in an NServiceBus application is made from message handlers. Each class typically handles one specific type of message. Testing these classes usually focuses on their externally visible behavior: the types of messages they send or reply with. This is as simple to test as could be expected:

6-pre NServiceBus.Testing
[TestFixture]
public class Tests
{
    [Test]
    public void Run()
    {
        Test.Handler<MyHandler>()
            .ExpectReply<ResponseMessage>(m => m.String == "hello")
            .OnMessage<RequestMessage>(m => m.String = "hello");
    }
}

public class MyHandler :
    IHandleMessages<RequestMessage>
{
    public Task Handle(RequestMessage message, IMessageHandlerContext context)
    {
        var reply = new ResponseMessage
        {
            String = message.String
        };
        return context.Reply(reply);
    }
}
4.x - 5.x NServiceBus.Testing
[TestFixture]
public class Tests
{
    [Test]
    public void Run()
    {
        Test.Initialize();

        Test.Handler<MyHandler>()
            .ExpectReply<ResponseMessage>(m => m.String == "hello")
            .OnMessage<RequestMessage>(m => m.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);
    }
}

This test says 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.

Testing a Saga

6-pre NServiceBus.Testing
[Test]
public void Run()
{
    Test.Saga<MySaga>()
            .ExpectReplyToOriginator<MyResponse>()
            .ExpectTimeoutToBeSetIn<StartsSaga>((state, span) => span == TimeSpan.FromDays(7))
            .ExpectPublish<MyEvent>()
            .ExpectSend<MyCommand>()
        .When((s, context) => s.Handle(new StartsSaga(), context))
            .ExpectPublish<MyEvent>()
        .WhenSagaTimesOut()
            .AssertSagaCompletionIs(true);
}
4.x - 5.x NServiceBus.Testing
[Test]
public void Run()
{
    Test.Initialize();
    Test.Saga<MySaga>()
            .ExpectReplyToOriginator<MyResponse>() // In version 4 the typo in Originator was fixed.
            .ExpectTimeoutToBeSetIn<StartsSaga>((state, span) => span == TimeSpan.FromDays(7))
            .ExpectPublish<MyEvent>()
            .ExpectSend<MyCommand>()
        .When(s => s.Handle(new StartsSaga()))
            .ExpectPublish<MyEvent>()
        .WhenSagaTimesOut()
            .AssertSagaCompletionIs(true);
}

Configuring unobtrusive message conventions

In Versions 5 and below, if unobtrusive mode is used, it must be configured with those conventions as shown below.

5.x NServiceBus.Testing
Test.Initialize(
    customisations: config =>
    {
        var conventions = config.Conventions();
        conventions.DefiningMessagesAs(MyMessageTypeConvention);
    });
4.x NServiceBus.Testing
MessageConventionExtensions.IsMessageTypeAction = MyMessageTypeConvention;
Test.Initialize();
3.x NServiceBus.Testing
MessageConventionExtensions.IsMessageTypeAction = MyMessageTypeConvention;

Test.Initialize();
In Versions 6 and above, unobtrusive message conventions are no longer required to use the NServiceBus Testing Framework.

Testing interface messages

To support testing of interface messages Version 5 introduces a .WhenHandling<T>() method where T is the interface type.

Testing header manipulation

It is the responsibility of the message handlers in the service layer to use data from headers found in the request to make decisions, and to set headers for the response messages. This is how such functionality can be tested:

6-pre NServiceBus.Testing
[TestFixture]
public class Tests
{
    [Test]
    public void Run()
    {
        Test.Handler<MyMessageHandler>()
            .SetIncomingHeader("MyHeaderKey", "myHeaderValue")
            .ExpectReply<ResponseMessage>((m, replyOptions) =>
                replyOptions.GetHeaders()["MyHeaderKey"] == "myHeaderValue")
            .OnMessage<RequestMessage>(m => m.String = "hello");
    }
}

class MyMessageHandler :
    IHandleMessages<RequestMessage>
{
    public Task Handle(RequestMessage message, IMessageHandlerContext context)
    {
        var header = context.MessageHeaders["MyHeaderKey"];

        var responseMessage = new ResponseMessage();

        var options = new ReplyOptions();
        options.SetHeader("MyHeaderKey", header);
        return context.Reply(responseMessage, options);
    }
}
4.x - 5.x NServiceBus.Testing
[TestFixture]
public class Tests
{
    [Test]
    public void Run()
    {
        Test.Initialize();

        Test.Handler<MyMessageHandler>()
            .SetIncomingHeader("MyHeaderKey", "myHeaderValue")
            .ExpectReply<ResponseMessage>(m => Test.Bus.GetMessageHeader(m, "MyHeaderKey") == "myHeaderValue")
            .OnMessage<RequestMessage>(m => m.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. Here's how:

6-pre NServiceBus.Testing
[TestFixture]
public class Tests
{
    [Test]
    public void RunWithDependencyInjection()
    {
        var mockService = new MyService();
        var handler = Test.Handler(new WithDependencyInjectionHandler(mockService));
        // Rest of test
    }
}

class WithDependencyInjectionHandler :
    IHandleMessages<MyMessage>
{
    MyService myService;

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

    public Task Handle(MyMessage message, IMessageHandlerContext context)
    {
        // use myService here
        return Task.FromResult(0);
    }
}
4.x - 5.x NServiceBus.Testing
[TestFixture]
public class Tests
{
    [Test]
    public void RunWithDependencyInjection()
    {
        Test.Initialize();

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

class WithDependencyInjectionHandler :
    IHandleMessages<MyMessage>
{
    MyService myService;

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

    public void Handle(MyMessage message)
    {
    }
}

Constructor injected bus

In Versions 6 and higher, the IBus interface was deprecated and removed, and replaced with the contextual IMessageHandlerContext parameter on the IHandleMessages<T>.Handle() methods.
4.x - 5.x NServiceBus.Testing
[TestFixture]
public class Tests2
{
    [Test]
    public void RunWithConstructorInjectedBus()
    {
        Test.Initialize();

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

class WithConstructorInjectedBusHandler :
    IHandleMessages<MyMessage>
{
    IBus bus;

    public WithConstructorInjectedBusHandler(IBus bus)
    {
        this.bus = bus;
    }
    public void Handle(MyMessage message)
    {
    }
}

Samples

Related Articles


Last modified