My Favorite NServiceBus Features - Testing

My Favorite NServiceBus Features - Testing
by Brad Jolicoeur
09/09/2020

Testing is one of the more challenging aspects of message based systems and is probably one of the top reasons it is common to see http endpoints instead of messaging in microservices. If you have an http endpoint, it is easy to use Postman or the Swagger UI to manually test your endpoint. Conversely, if you try to do the same thing in a message based system, first you would need to create a queue, then you would need to carefully craft a message and put it in the queue to read. After you've manually generated a message or two you will probably decide to create a test harness to generate the messages for you, but you will still have the setup and teardown of queues.

The Pain

If your system uses a cloud based transport like Azure Service Bus that does not have a local emulator, you may be tempted to share a set of queues in your development environment. If there is only one developer on the team, you might be able to get away with this, but when you expand out to have more than one developer, add in QA testers and your automated tests in your CI/CD pipeline. If all of these are sharing the same set of queues, you are on the pain train my friend. Some days may go smooth, but other days everyone will collide when messages you produce for testing are consumed by someone else on your team. In this case, both of you will be scratching your heads trying to figure out why something is not working. Pile on the fact that your automated tests keep failing, because they picked up a message they don't yet know how to process or you consumed the message the test was waiting for.

If you build your own messaging framework, the challenges around testing will be an aspect that does not show up initially. Your early POC and MVP will likely discount testing to some extent since it is not a core requirement from your stakeholders. Once you move into production and your stakeholders get burned by an instable system that has limited testing, they will quickly insist that more testing is done and this is when the fun will begin. This is also when the size of your team will start to ramp up to build on the success of your earlier initiatives.

NServiceBus to the Rescue

If you chose NServiceBus instead of building your own framework or going with one of the other frameworks out there with a limited set of functionality, you are in luck. NServiceBus includes a testing framework that will allow you to backfill your automated testing without the need to modify your code, setup/teardown queues or deal with shared queues. This framework will work within your already established unit testing framework and provides an NServiceBus endpoint context to send messages as well as assert that messages were sent by your handler. Once again NServiceBus has thrown you into the pit-of-success by providing a feature you didn't know you needed in the onset.

One of the first pushbacks I typically get when I introduce the NServiceBus testing framework is that we are not testing the interaction with the queues. While this is true, it is not the concern it might be if you had created your own messaging framework. If you are using NServiceBus the transport and framework have already been tested before you integrated it into your system.

There is little value in testing the NServiceBus framework. You will get the bulk of your testing value by focusing on the business logic and customized behaviors you have created on top of the framework. Remember that NServiceBus is crafted by people who's primary job is to make NServiceBus as stable and friction free as possible. This is a level of focus and expertise you will never be able to achieve with your inhouse messaging framework.

The NServiceBus testing framework will not devoid you of the requirement to have a set of end-to-end tests. You will need a small set of end-to-end tests to verify that your queues are all set up properly as well as routing. Just remember that end-to-end are at the top of the Test Pyramid. I've seen at least one nicely decoupled microservice implementation that was completely bound by a vast set of fragile end-to-end tests that took hours to run and failed so often no one noticed until it stopped us from deploying.

Testing a Handler

The easiest way to describe how the NServiceBus Testing framework will help you is by starting with an example. Below is a simple example of a handler and the associated test from the NServiceBus documentation.

Example message handler

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

Example test

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

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

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

The TestableMessageHandlerContext is essentially a mock implementation of an NServiceBus IMessageHandlerContext. This allows your message handler to operate like it has access to the endpoint context and do things like send or reply to messages. Once the handler has been invoked, you can then do assertions on the messages that should have been sent out along with any of the other assertions to test the logic of your handler is working properly.

At this point you may be thinking, "but we used a mock so we didn't do a full test."

Remember, that the code that is being mocked is from a library you purchased. Testing NServiceBus is the responsibility of Particular and you want to focus your tests on the business logic you put in your handler. This is where you will get the majority of the benefits of testing.

Notice that there is no setup and teardown of queues involved in testing your business logic here. This will happily and reliably run in your CI/CD pipeline with no ceremony.

Testing Sagas

Testing Sagas is even more complicated when you consider testing against live queues. This is because as a long running workflow, state is an important aspect of testing your handlers. To test a Saga using live queues, you will need to do some elaborate setup and hope some other test running simultaneously doesn't interfere with your setup.

With the TestableMessageHandlerContext you can set the saga state specifically for your saga message handler tests. This is illustrated in another example from the NServiceBus documentation.

[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)
        .ConfigureAwait(false);

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

In this example, the order discount logic in the saga is tested. Imagine that the discount was dependent upon the total amount of orders the customer made in the last week, the saga data could be set to that amount in the arrange section of the test to verify that the discount is applied correctly. This would be a scenario that would be very difficult and time consuming to set up. It is also the kind of test that doesn't work when you get back from vacation and haven't run any tests in the last week. Fun times.

Testing Behaviors

Another great NServiceBus feature is message pipeline behaviors. These behaviors can be included in the inbound or outbound message pipeline to implement cross-cutting functionality. This is similar to ASP.NET Action Filters and can be used for things like consuming or applying a specific message header to all your outbound messages.

Due to the cross-cutting nature of behaviors, you will typically put them in a shared assembly that is distributed with a NuGet package. The ability to test these behaviors in isolation is extremely valuable since it will help you insure your behavior is ready to be consumed by multiple endpoints. The alternative would be to make your change package it, pull it into a consuming endpoint and invoke that behavior in the endpoint. If you are lucky that works. If not, you have to throw it against the wall again and in the mean time hope no one else on your team pulls in that broken package.

Below is an example of a behavior and test for reference. There are a couple of nuances to testing behavior so you definitely want to check out the documentation before you dive in.

Custom Behavior

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

Behavior Test

[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"]);
}

Fluent Style Tests

If you spend any time searching the web about NServiceBus testing, you will find references to a fluent style of testing that NServiceBus offers. While I am generally a fan of the builder pattern, this is not the greatest one to work with since it doesn't readily follow the arrange-act-assert pattern of testing. If you look closely at Particular's advice on the matter, they moved away from the fluent style tests in version 6 of NServiceBus for an implementation that is more compatible with the arrange-act-assert pattern and left the fluent style for backwards compatibility of existing tests.

I have used both styles and while the fluent style works ok with simple scenarios, it quickly starts to fall apart when you get into more advanced scenarios. The big downfall is that it mixes the act and assert phases of the test and validating specific properties were set correctly on an outbound message gets messy. Based on this experience, I recommend you follow Particular's advice and avoid the fluent style for anything new. If you have existing tests that use the fluent style, I would not go back and refactor them, unless there were other reasons to modify the tests.

Below are side-by-side examples showing the fluent style vs the new version of the testing framework I found on the Particular website.

Test using fluent style

[Test]
public void TestHandler()
{
    Test.Handler(new SampleHandler(new SomeDependency()))
        .ExpectPublish<Started>(msg => msg.SomeId == 123)
        .ExpectNotSend<ContinueProcess>(msg => true)
        .OnMessage(new StartProcess { SomeId = 123 });
}

Test using new framework

[Test]
public async Task TestHandlerAAA()
{
    // Arrange
    var handler = new SampleHandler(new SomeDependency());
    var context = new NServiceBus.Testing.TestableMessageHandlerContext();

    // Act
    await handler.Handle(new StartMsg {SomeId = 123}, context);

    // Assert
    Assert.AreEqual(1, context.PublishedMessages.Length);
    Assert.AreEqual(0, context.SentMessages.Length);
    var started = context.PublishedMessages[0].Message as Started;
    Assert.AreEqual("test", started.SomeId);
}

As you can see the newer testing framework is more verbose, but it is easier to understand what is happening. Consider that this test only asserts the id property is correct. If there are other properties you need to assert, the fluent style gets much harder to read and even harder to diagnose what is going on when the test fails.

Summary

The NServiceBus Testing framework takes most of the pain out of creating automated tests for your message based endpoints. This means your message based system is more likely to have meaningful tests that reliably run. The NServiceBus testing framework is another example of NServiceBus throwing you into the pit-of-success by providing you with a feature you didn't know you needed until late in the game.

Resources

You May Also Like


Transform JSON with ChatGPT

PalaceLight.JPG
Brad Jolicoeur - 11/17/2024
Read

Convert HTML into JSON using Semantic Kernel and OpenAI

solvingsomethingawesome.jpg
Brad Jolicoeur - 09/28/2024
Read

Fabricate Sample Data with ChatGPT

fall-road.jpg
Brad Jolicoeur - 08/24/2024
Read