Skip to content

Create a unit test

MassTransit is an asynchronous framework that enables the development of high-performance and flexible distributed applications. Because of MassTransit’s asynchronous underpinning, unit testing consumers, sagas, and routing slip activities can be significantly more complex. To simplify the creation of unit and integration tests, MassTransit includes a Test Harness that simplifies test creation.

  • Simplifies configuration for a majority of unit test scenarios
  • Provides an in-memory transport, saga repository, and message scheduler
  • Exposes published, sent, and consumed messages
  • Supports Web Application Factory for testing ASP.NET Applications

As stated above, MassTransit is an asynchronous framework. In most cases, developers want to test that message consumption is successful, consumer behavior is as expected, and messages are published and/or sent. Because these actions are performed asynchronously, MassTransit’s test harness exposes several asynchronous collections allowing test assertions verifying developer expectations. These asynchronous collections are backed by an over test timer and an inactivity timer, so it’s important to use a test harness only once for a given scenario. Multiple test assertions, messages, and behaviors are normal in a given test, but unrelated scenarios should not share a single test harness.

Unit testing is a good practice to ensure that your code works as expected, and MassTransit’s test harness makes it easy to test your consumers (including consumers, job consumers, saga state machines, and routing slip activities).

A unit test uses the Microsoft.Extensions.DependencyInjection container to configure the test harness. Using the ServiceCollection extension method AddMassTransitTestHarness adds the test harness to the container. The test harness is then started using the StartTestHarness method. When the ServiceProvider returned by BuildServiceProvider is disposed, the test harness is automatically stopped.

[Test]
public async Task A_complete_unit_test()
{
await using var provider = new ServiceCollection()
.AddMassTransitTestHarness(x =>
{
})
.BuildServiceProvider(true);
var harness = await provider.StartTestHarness();
}

The unit test above is the minimum required to get started with the test harness. It assumes that the in-memory transport is used, the delayed message scheduler is configured, and automatically calls ConfigureEndpoints on the bus to configure the receive endpoints

A more verbose example is shown below.

[Test]
public async Task A_complete_unit_test()
{
await using var provider = new ServiceCollection()
.AddMassTransitTestHarness(x =>
{
x.AddDelayedMessageScheduler();
x.UsingInMemory((context, cfg) =>
{
cfg.UseDelayedMessageScheduler();
cfg.ConfigureEndpoints(context));
});
})
.BuildServiceProvider(true);
var harness = await provider.StartTestHarness();
}

In addition to the default in-memory transport, the test harness can be used with any supported transport. This might be useful for testing using an actual message transport in an integration test where a transport-specific feature is required.

For example, to use Azure Service Bus as the transport and test a subscription endpoint, add the following to the AddMassTransitTestHarness method.

[Test]
public async Task An_azure_service_bus_unit_test()
{
await using var provider = new ServiceCollection()
.AddMassTransitTestHarness(x =>
{
x.AddServiceBusMessageScheduler();
x.AddConsumer<AuditOrderCreatedConsumer>();
x.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host("sb:///my-namespace.servicebus.windows.net"));
cfg.UseServiceBusMessageScheduler();
cfg.SubscriptionEndpoint<OrderCreated>("audit-order-created", e =>
{
e.ConfigureConsumer<AuditOrderCreatedConsumer>(context);
});
cfg.ConfigureEndpoints(context));
});
})
.BuildServiceProvider(true);
var harness = await provider.StartTestHarness();
await harness.Scope.ServiceProvider.GetRequiredService<IPublishEndpoint>().Publish(new OrderCreated());
Assert.That(await harness.Consumed.Any<OrderCreated>());
}

To verify that messages were sent, published, or consumed, you can use the Select or SelectAsync methods on the Sent, Published, or Consumed collections.

var messageSent = await harness.Sent.SelectAsync<OrderSubmitted>()
.FirstOrDefault();
Assert.AreEqual(orderId, messageSent?.Context.Message.OrderId);

v9.1+

MassTransit 9.1+ provides a new Act method on the test harness to execute actions and encapsulate the observed results. This can significantly speed up tests that perform negative assertions, such as messages not being consumed, published, or sent.

As an example, tests within the MassTransit test suite changed to use the Act method went from taking 1.6 seconds to 60 milliseconds! That’s a huge improvement, reducing the inner loop time significantly.

Consider the following test, which has been rewritten to use the Act method.

[Test]
public async Task Should_exclude_consumer_by_attribute()
{
await using var provider = new ServiceCollection()
.AddMassTransitTestHarness(x => x.AddConsumer<PongConsumer>())
.BuildServiceProvider(true);
var harness = await provider.StartTestHarness();
var result = await harness.Act(async x =>
{
await x.Bus.Publish<Ping>(new { }, x.CancellationToken);
});
Assert.That(await result.Consumed.Any<Ping>(), Is.False);
}

The Act method returns a TestHarnessResult object, which contains the observed results of the actions performed within the Act method. The results are already completed, and any assertions are performed immediately ignoring both the TestTimeout and the TestInactivityTimeout.

Internally, the Act method uses Open Telemetry to trace the actions performed and ensures that the trace, including any further spans, is completed before completing and returning the TestHarnessResult.

There are two configurable timeouts in the test harness.

  • TestInactivityTimeout is used by the Consumed, Sent, and Published observer collections to limit the time spent waiting for messages to be added to the collection.
  • TestTimeout is the overall test timeout, which is the outermost limit for the unit test operations that wait for asynchronous operations to complete.

In the example above, the await harness.Sent.Any<OrderSubmitted>() method call waits for the OrderSubmitted message to be sent within the time specified by the TestInactivityTimeout property of the Test Harness. The default timeout is 1.2 seconds, and 30 minutes while debugging, configured by the TestHarnessOptions from the container.

public class TestHarnessOptions
{
public TimeSpan TestTimeout { get; set; }
public TimeSpan TestInactivityTimeout { get; set; }
}

To set both the overall test timeout and the test inactivity timeout, call the SetTestTimeouts method (within the AddMassTransit configuration block):

x.SetTestTimeouts(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(5)));

Either timeout value can be set individually using the appropriately named parameter:

x.SetTestTimeouts(testTimeout: TimeSpan.FromSeconds(60));
x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5));

v9.1+

Longer running tests can cause the test harness to consume a lot of memory when a large number of messages are consumed, published, and sent. To reduce the memory footprint, the test harness can be configured to save the full context (default), only the message, or only the minimal data required to identify the message.

services.AddMassTransitTestHarness(x =>
{
// to save only the message
x.SetTestContextSaveMode(TestContextSaveMode.MessageOnly);
// to save only the minimal data required to identify the message
x.SetTestContextSaveMode(TestContextSaveMode.NoContext);
});