Testing NServiceBus Message Handlers

April 15, 2024#Software Development
Series: NServiceBus
Article
Author image.

Kyle McMaster, Senior Consultant

This post is the fourth in a series on NServiceBus, a popular messaging framework for building distributed systems. If you’ve missed any of the previous articles, be sure to check out the series page for all the related posts. In the previous post, we covered the different types of messages and how to utilize them in your NServiceBus applications. In this post, we’ll take a step away from messaging-related features and focus on another important topic, Unit Testing! We’ll look at how to write unit tests for NServiceBus message handlers using some of the utilities from the NServiceBus.Testing version package at version 8.1.0 at the time of writing.

Verifying Side Effects with Unit Tests 🔬

Automated testing is a foundational practice for us at NimblePros. As a firm believer and practitioner of automated testing, I believe unit testing is a crucial part of the software development process. It allows developers to verify that their code behaves as expected under different conditions and prevent regressions. When it comes to NServiceBus classes that implement the IHandleMessages<T> interface known as message handlers, unit tests can help ensure that the handlers are processing messages correctly and producing any expected side effects. In this article, our focus will be on verifying that an instance of the IMessageHandlerContext is called whenever we expect to send a message or publish an event. We can even verify that the message being sent or published is the one we expect and has the correct data. We’ll use the following message handler from the previous post as an example:

public class ContributorCreateCommandHandler(IRepository<Contributor> _repository) 
  : IHandleMessages<ContributorCreateCommand>
{
  public async Task Handle(ContributorCreateCommand message, IMessageHandlerContext context)
  {
    var phoneNumber = new PhoneNumber(string.Empty, message.PhoneNumber, string.Empty);
    var contributor = new Contributor(message.Name, phoneNumber, ContributorStatus.NotSet);
    await _repository.AddAsync(contributor, context.CancellationToken);

    await context.Publish(new ContributorCreatedEvent
    {
      ContributorId = contributor.Id,
      Name = contributor.Name,
      Status = contributor.Status.ToString()
    });
  }
}

Writing Unit Tests for IMessageHandlers ✅

To write unit tests for NServiceBus message handlers, we can use the NServiceBus.Testing package. This package provides utilities for testing message handlers in isolation, without the need to set up an entire NServiceBus endpoint. The package includes a TestableMessageHandlerContext class that simulates the behavior of an IMessageHandlerContext and allows us to verify the interactions between the message handler and the context. We’ll skip any additional tests you may want to write like asserting that the repository receives a call to AddAsync() to focus on just the NServiceBus functionality now, but I recommend including those tests as well in your test suite. Here’s a walkthrough of the process of writing a unit test for the ContributorCreateCommandHandler.

This test follows the AAA style of unit testing. First, our test method sets up all the necessary objects and data. Then, we call the system under test (SUT) which is the ContributorCreateCommandHandler in this case, to act on its Handle method. Finally, we assert that the expected side effect occurred, which is that the ContributorCreatedEvent was published. The TestableMessageHandlerContext exposes properties such as SentMessages and PublishedMessages that we can use to verify that the message handler interacted with the context as expected. The following iteration of the test method verifies a single ContributorCreatedEvent is published. This code sample uses XUnit and NSubstitute. However, you can choose which testing frameworks and tools for your own. The key thing to notice is the TestableMessageHandlerContext that is provided by the NServiceBus.Testing package.

[Fact]
public async Task ShouldPublishContributorCreatedEvent()
{
  var message = new ContributorCreateCommand
  {
    Name = "Test Contributor",
    PhoneNumber = "123-456-7890"
  };
  var phoneNumber = new PhoneNumber(string.Empty, message.PhoneNumber, string.Empty);
  var contributor = new Contributor(message.Name, phoneNumber, ContributorStatus.NotSet)
  {
    Id = 1
  };
  var repository = Substitute.For<IRepository<Contributor>>();
  repository
    .AddAsync(Arg.Any<Contributor>(), Arg.Any<CancellationToken>())
    .Returns(contributor);
  var context = new TestableMessageHandlerContext();
  var handler = new ContributorCreateCommandHandler(repository);

  await handler.Handle(message, context);

  context.PublishedMessages.Single().Message.Should().BeOfType<ContributorCreatedEvent>();
}

Depending on your confidence in your mapping code, you may also add assertions to verify the data in the published message. This requires a little more staging since we need to set up NSubstitute to return a contributor based on the message data but it’s not too much more work. Notice how we’re able to check the properties of the published message using the As<T>() extension method from the FluentAssertions library. Here’s the updated test method that verifies the properties of the published ContributorCreatedEvent:

[Fact]
public async Task ShouldPublishContributorCreatedEvent()
{
  var message = new ContributorCreateCommand
  {
    Name = "Test Contributor",
    PhoneNumber = "123-456-7890"
  };
  var phoneNumber = new PhoneNumber(string.Empty, message.PhoneNumber, string.Empty);
  var contributor = new Contributor(message.Name, phoneNumber, ContributorStatus.NotSet)
  {
    Id = 1
  };
  var repository = Substitute.For<IRepository<Contributor>>();
  repository
    .AddAsync(Arg.Any<Contributor>(), Arg.Any<CancellationToken>())
    .Returns(contributor);
  var context = new TestableMessageHandlerContext();
  var handler = new ContributorCreateCommandHandler(repository);

  await handler.Handle(message, context);

  using var assertionScope = new AssertionScope();
  var publishedMessage = context.PublishedMessages.Single();
  publishedMessage.Message.Should().BeOfType<ContributorCreatedEvent>();
  publishedMessage.Message.As<ContributorCreatedEvent>().ContributorId.Should().Be(1);
  publishedMessage.As<ContributorCreatedEvent>().Name.Should().Be(message.Name);
  publishedMessage.As<ContributorCreatedEvent>().Status.Should().Be(ContributorStatus.NotSet.Name);
}

A quick note on the NServiceBus.Testing package 📝

As seen above, this package provides TestableMessageHandlerContext that enables us to write assertions based on its properties. Since the IMessageSession is also popular in NServiceBus applications, I thought I’d mention that the package also includes a TestableMessageSession as well as many more “testable” classes for faking other NServiceBus components. You can find more information on the NServiceBus.Testing package in the official documentation.

Wrapping up! 🎁

In this post, we’ve covered how to a write unit test for NServiceBus message handlers using the NServiceBus.Testing package. We’ve seen how to use the TestableMessageHandlerContext class to simulate the behavior of an IMessageHandlerContext and verify the interactions between the message handler and the context. By writing unit tests for message handlers, you can ensure that your NServiceBus endpoints are processing messages correctly and producing the expected side effects. Stay tuned for more content in this series, as we’ll be exploring more advanced topics like Sagas and patterns like Unit of Work and Outbox in future posts.

Resources 📚


Copyright © 2024 NimblePros - All Rights Reserved