Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Wolverine and other JasperFx libraries. Please check our Support Plans for more details.

Test Automation Support

The Wolverine team absolutely believes in Test Driven Development and the importance of strong test automation strategies as a key part of sustainable development. To that end, Wolverine's conceptual design from the very beginning (Wolverine started as "Jasper" in 2015!) has been to maximize testability by trying to decouple application code from framework or other infrastructure concerns.

See Jeremy's blog post How Wolverine allows for easier testing for an introduction to unit testing Wolverine message handlers.

Also see Wolverine Best Practices for other helpful tips.

And this:

Integration Testing with Tracked Sessions

TIP

This is the recommended approach for integration testing against Wolverine message handlers if there are any outgoing messages or asynchronous behavior as a result of the messages being handled in your test scenario.

INFO

As of Wolverine 3.13, the same extension methods shown here are available off of IServiceProvider in addition to the original support off of IHost if you happen to be writing integration tests by spinning up just an IoC container and not the full IHost in your test harnesses.

So far we've been mostly focused on unit testing Wolverine handler methods individually with unit tests without any direct coupling to infrastructure. Great, that's a great start, but you're eventually going to also need some integration tests, and invoking or publishing messages is a very logical entry point for integration testing.

First, why integration testing with Wolverine?

  1. Wolverine is probably most effective when you're heavily leveraging middleware or Wolverine conventions, and only an integration test is really going to get through the entire "stack"
  2. You may frequently want to test the interaction between your application code and infrastructure concerns like databases
  3. Handling messages will frequently spawn other messages that will be executed in other threads or other processes, and you'll frequently want to write bigger tests that span across messages

TIP

I'm not getting into it here, but remember that IHost is relatively expensive to build, so you'll probably want it cached between tests. Or at least be aware that it's expensive.

This sample was taken from an introductory blog post that may give you some additional context for what's happening here.

Going back to our sample message handler for the DebitAccount in the previous sections, let's say that we want an integration test that spans the middleware that looks up the Account data, the Fluent Validation middleware, Marten usage, and even across to any cascading messages that are also handled in process as a result of the original message. One of the big challenges with automated testing against asynchronous processing is knowing when the "action" part of the "arrange/act/assert" phase of the test is complete and it's safe to start making assertions. Anyone who has had the misfortune to work with complicated Selenium test suites is very aware of this challenge.

Not to fear though, Wolverine comes out of the box with the concept of "tracked sessions" that you can use to write predictable and reliable integration tests.

WARNING

I'm omitting the code necessary to set up system state first just to concentrate on the Wolverine mechanics here.

To start with tracked sessions, let's assume that you have an IHost for your Wolverine application in your testing harness. Assuming you do, you can start a tracked session using the IHost.InvokeMessageAndWaitAsync() extension method in Wolverine like this:

cs
public async Task using_tracked_sessions()
{
    // The point here is just that you somehow have
    // an IHost for your application
    using var host = await Host.CreateDefaultBuilder()
        .UseWolverine().StartAsync();

    var debitAccount = new DebitAccount(111, 300);
    var session = await host.InvokeMessageAndWaitAsync(debitAccount);

    var overdrawn = session.Sent.SingleMessage<AccountOverdrawn>();
    overdrawn.AccountId.ShouldBe(debitAccount.AccountId);
}

snippet source | anchor

The tracked session mechanism utilizes Wolverine's internal instrumentation to "know" when all the outstanding work in the system is complete. In this case, if the AccountOverdrawn message spawned from DebitAccount is handled locally, the InvokeMessageAndWaitAsync() call will not return until the other messages that are routed locally are finished processing or the test times out. The tracked session will also throw an AggregateException with any exceptions encountered by any message being handled within the activity that is tracked.

Note that you'll probably mostly invoke messages in these tests, but there are additional extension methods on IHost for other IMessageBus operations.

INFO

The Tracked Session includes only messages sent, published, or scheduled during the tracked session. Messages sent before the tracked session are not included in the tracked session.

Finally, there are some more advanced options in tracked sessions you may find useful as shown below:

cs
public async Task using_tracked_sessions_advanced(IHost otherWolverineSystem)
{
    // The point here is just that you somehow have
    // an IHost for your application
    using var host = await Host.CreateDefaultBuilder()
        .UseWolverine().StartAsync();

    var debitAccount = new DebitAccount(111, 300);
    var session = await host

        // Start defining a tracked session
        .TrackActivity()

        // Override the timeout period for longer tests
        .Timeout(1.Minutes())

        // Be careful with this one! This makes Wolverine wait on some indication
        // that messages sent externally are completed
        .IncludeExternalTransports()

        // Make the tracked session span across an IHost for another process
        // May not be super useful to the average user, but it's been crucial
        // to test Wolverine itself
        .AlsoTrack(otherWolverineSystem)

        // This is actually helpful if you are testing for error handling
        // functionality in your system
        .DoNotAssertOnExceptionsDetected()
        
        // Hey, just in case failure acks are getting into your testing session
        // and you do not care for the tests, tell Wolverine to ignore them
        .IgnoreFailureAcks()

        // Again, this is testing against processes, with another IHost
        .WaitForMessageToBeReceivedAt<LowBalanceDetected>(otherWolverineSystem)
        
        // Wolverine does this automatically, but it's sometimes
        // helpful to tell Wolverine to not track certain message
        // types during testing. Especially messages originating from
        // some kind of polling operation
        .IgnoreMessageType<IAgentCommand>()
        
        // Another option
        .IgnoreMessagesMatchingType(type => type.CanBeCastTo<IAgentCommand>())

        // There are many other options as well
        .InvokeMessageAndWaitAsync(debitAccount);

    var overdrawn = session.Sent.SingleMessage<AccountOverdrawn>();
    overdrawn.AccountId.ShouldBe(debitAccount.AccountId);
}

snippet source | anchor

The samples shown above inlcude Sent message records, but there are more properties available in the TrackedSession object. In accordance with the MessageEventType enum, you can access these properties on the TrackedSession object:

cs
public enum MessageEventType
{
    Received,
    Sent,
    ExecutionStarted,
    ExecutionFinished,
    MessageSucceeded,
    MessageFailed,
    NoHandlers,
    NoRoutes,
    MovedToErrorQueue,
    Requeued,
    Scheduled
}

snippet source | anchor

Let's consider we're testing a Wolverine application which publishes a message, when a change to a watched folder is detected. The part we want to test is that a message is actually published when a file is added to the watched folder. We can use the TrackActivity method to start a tracked session and then use the ExecuteAndWaitAsync method to wait for the message to be published when the file change has happened.

cs
public record FileAdded(string FileName);

public class FileAddedHandler
{
    public Task Handle(
        FileAdded message
    ) =>
        Task.CompletedTask;
}

public class RandomFileChange
{
    private readonly IMessageBus _messageBus;

    public RandomFileChange(
        IMessageBus messageBus
    ) => _messageBus = messageBus;

    public async Task SimulateRandomFileChange()
    {
        // Delay task with a random number of milliseconds
        // Here would be your FileSystemWatcher / IFileProvider
        await Task.Delay(
            TimeSpan.FromMilliseconds(
                new Random().Next(100, 1000)
            )
        );
        var randomFileName = Path.GetRandomFileName();
        await _messageBus.SendAsync(new FileAdded(randomFileName));
    }
}

public class When_message_is_sent : IAsyncLifetime
{
    private IHost _host;

    public async Task InitializeAsync()
    {
        var hostBuilder = Host.CreateDefaultBuilder();
        hostBuilder.ConfigureServices(
            services => { services.AddSingleton<RandomFileChange>(); }
        );
        hostBuilder.UseWolverine();

        _host = await hostBuilder.StartAsync();
    }
    
    [Fact]
    public async Task should_be_in_session_using_service_provider()
    {
        var randomFileChange = _host.Services.GetRequiredService<RandomFileChange>();

        var session = await _host.Services
            .TrackActivity()
            .Timeout(2.Seconds())
            .ExecuteAndWaitAsync(
                (Func<IMessageContext, Task>)(
                    async (
                        _
                    ) => await randomFileChange.SimulateRandomFileChange()
                )
            );

        session
            .Sent
            .AllMessages()
            .Count()
            .ShouldBe(1);
        
        session
            .Sent
            .AllMessages()
            .First()
            .ShouldBeOfType<FileAdded>();
    }

    [Fact]
    public async Task should_be_in_session()
    {
        var randomFileChange = _host.Services.GetRequiredService<RandomFileChange>();

        var session = await _host
            .TrackActivity()
            .Timeout(2.Seconds())
            .ExecuteAndWaitAsync(
                (Func<IMessageContext, Task>)(
                    async (
                        _
                    ) => await randomFileChange.SimulateRandomFileChange()
                )
            );

        session
            .Sent
            .AllMessages()
            .Count()
            .ShouldBe(1);
        
        session
            .Sent
            .AllMessages()
            .First()
            .ShouldBeOfType<FileAdded>();
    }

    public async Task DisposeAsync() => await _host.StopAsync();
}

snippet source | anchor

As you can see, we just have to start our application, attach a tracked session to it, and then wait for the message to be published. This way, we can test the whole process of the application, from the file change to the message publication, in a single test.

Dealing with Scheduled Messages 4.12

As I'm sure you can imagine, scheduled local execution and scheduled message delivery can easily be confusing for testing and have occasionally caused trouble for Wolverine users using the tracked session functionality. At this point, Wolverine now tracks any scheduled messages a little separately under an ITrackedSession.Scheduled collection, and any message that is scheduled for later execution or delivery is automatically interpreted as "complete" in the tracked session.

You can also force the "tracked session" to immediately "replay" any scheduled messages tracked in the original session by:

  1. Invoking any messages that were scheduled for local execution
  2. Sending any messages that were scheduled for delivery to the original destination

and returning a brand new tracked session for the "replay."

Here's an example from our test suite. First though, here's the message handlers in question (remember, this is rigged up for testing):

cs
public static DeliveryMessage<ScheduledMessage> Handle(TriggerScheduledMessage message)
{
    // This causes a message to be scheduled for delivery in 5 minutes from now
    return new ScheduledMessage(message.Text).DelayedFor(5.Minutes());
}

public static void Handle(ScheduledMessage message) => Debug.WriteLine("Got scheduled message");

snippet source | anchor

And the test that exercises this functionality:

cs
// In this case we're just executing everything in memory
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.PersistMessagesWithPostgresql(Servers.PostgresConnectionString, "wolverine");
        opts.Policies.UseDurableInboxOnAllListeners();
    }).StartAsync();

// Should finish cleanly
var tracked = await host.SendMessageAndWaitAsync(new TriggerScheduledMessage("Chiefs"));

// Here's how you can query against the messages that were detected to be scheduled
tracked.Scheduled.SingleMessage<ScheduledMessage>()
    .Text.ShouldBe("Chiefs");

// This API will try to immediately play any scheduled messages immediately
var replayed = await tracked.PlayScheduledMessagesAsync(10.Seconds());
replayed.Executed.SingleMessage<ScheduledMessage>().Text.ShouldBe("Chiefs");

snippet source | anchor

And now, a slightly more complicated test that tests the replay of a message scheduled to go to a completely separate application:

cs
var port1 = PortFinder.GetAvailablePort();
var port2 = PortFinder.GetAvailablePort();

using var sender = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.PublishMessage<ScheduledMessage>().ToPort(port2);
        opts.ListenAtPort(port1);
    }).StartAsync();

using var receiver = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.ListenAtPort(port2);
    }).StartAsync();

// Should finish cleanly
var tracked = await sender
    .TrackActivity()
    .IncludeExternalTransports()
    .AlsoTrack(receiver)
    .InvokeMessageAndWaitAsync(new TriggerScheduledMessage("Broncos"));

tracked.Scheduled.SingleMessage<ScheduledMessage>()
    .Text.ShouldBe("Broncos");

var replayed = await tracked.PlayScheduledMessagesAsync(10.Seconds());
replayed.Executed.SingleMessage<ScheduledMessage>().Text.ShouldBe("Broncos");

snippet source | anchor

Extension Methods for Outgoing Messages

Your Wolverine message handlers will often have some need to publish, send, or schedule other messages as part of their work. At the unit test level you'll frequently want to validate the decision about whether or not to send a message. To aid in those assertions, Wolverine out of the box includes some testing helper extension methods on IEnumerable<object> inspired by the Shouldly project.

For an example, let's look at this message handler for applying a debit to a bank account that will use cascading messages to raise a variable number of additional messages:

cs
[Transactional]
public static IEnumerable<object> Handle(
    DebitAccount command,
    Account account,
    IDocumentSession session)
{
    account.Balance -= command.Amount;

    // This just marks the account as changed, but
    // doesn't actually commit changes to the database
    // yet. That actually matters as I hopefully explain
    session.Store(account);

    // Conditionally trigger other, cascading messages
    if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
    {
        yield return new LowBalanceDetected(account.Id)
            .WithDeliveryOptions(new DeliveryOptions { ScheduleDelay = 1.Hours() });
    }
    else if (account.Balance < 0)
    {
        yield return new AccountOverdrawn(account.Id);

        // Give the customer 10 days to deal with the overdrawn account
        yield return new EnforceAccountOverdrawnDeadline(account.Id);
    }

    yield return new AccountUpdated(account.Id, account.Balance);
}

snippet source | anchor

The testing extensions can be seen in action by the following test:

cs
[Fact]
public void handle_a_debit_that_makes_the_account_have_a_low_balance()
{
    var account = new Account
    {
        Balance = 1000,
        MinimumThreshold = 200,
        Id = 1111
    };

    // Let's otherwise ignore this for now, but this is using NSubstitute
    var session = Substitute.For<IDocumentSession>();

    var message = new DebitAccount(account.Id, 801);
    var messages = AccountHandler.Handle(message, account, session).ToList();

    // Now, verify that the only the expected messages are published:

    // One message of type AccountUpdated
    messages
        .ShouldHaveMessageOfType<AccountUpdated>()
        .AccountId.ShouldBe(account.Id);

    // You can optionally assert against DeliveryOptions
    messages
        .ShouldHaveMessageOfType<LowBalanceDetected>(delivery =>
        {
            delivery.ScheduleDelay.Value.ShouldNotBe(TimeSpan.Zero);
        })
        .AccountId.ShouldBe(account.Id);

    // Assert that there are no messages of type AccountOverdrawn
    messages.ShouldHaveNoMessageOfType<AccountOverdrawn>();
}

snippet source | anchor

The supported extension methods so far are in the TestingExtensions class.

As we'll see in the next section, you can also find a matching Envelope for a message type.

TIP

I'd personally organize the testing against that handler with a context/specification pattern, but I just wanted to show the extension methods here.

TestMessageContext

TIP

This testing mechanism is admittedly just a copy of the test support in older messaging frameworks in .NET. It's only useful as an argument passed into a handler method. We recommend using the "Tracked Session" approach instead.

In the section above we used cascading messages, but since there are some use cases -- or maybe even just user preference -- that would lead you to directly use IMessageContext to send additional messages from a message handler, Wolverine comes with the TestMessageContext class that can be used as a test double spy within unit tests.

Here's a different version of the message handler from the previous section, but this time using IMessageContext directly:

cs
[Transactional]
public static async Task Handle(
    DebitAccount command,
    Account account,
    IDocumentSession session,
    IMessageContext messaging)
{
    account.Balance -= command.Amount;

    // This just marks the account as changed, but
    // doesn't actually commit changes to the database
    // yet. That actually matters as I hopefully explain
    session.Store(account);

    // Conditionally trigger other, cascading messages
    if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
    {
        await messaging.SendAsync(new LowBalanceDetected(account.Id));
    }
    else if (account.Balance < 0)
    {
        await messaging.SendAsync(new AccountOverdrawn(account.Id), new DeliveryOptions{DeliverWithin = 1.Hours()});

        // Give the customer 10 days to deal with the overdrawn account
        await messaging.ScheduleAsync(new EnforceAccountOverdrawnDeadline(account.Id), 10.Days());
    }

    // "messaging" is a Wolverine IMessageContext or IMessageBus service
    // Do the deliver within rule on individual messages
    await messaging.SendAsync(new AccountUpdated(account.Id, account.Balance),
        new DeliveryOptions { DeliverWithin = 5.Seconds() });
}

snippet source | anchor

To test this handler, we can use TestMessageContext as a stand in to just record the outgoing messages and even let us do some assertions on exactly how the messages were published. I'm using xUnit.Net here, but this is certainly usable from other test harness tools:

cs
public class when_the_account_is_overdrawn : IAsyncLifetime
{
    private readonly Account theAccount = new Account
    {
        Balance = 1000,
        MinimumThreshold = 100,
        Id = Guid.NewGuid()
    };

    private readonly TestMessageContext theContext = new TestMessageContext();

    // I happen to like NSubstitute for mocking or dynamic stubs
    private readonly IDocumentSession theDocumentSession = Substitute.For<IDocumentSession>();

    public async Task InitializeAsync()
    {
        var command = new DebitAccount(theAccount.Id, 1200);
        await DebitAccountHandler.Handle(command, theAccount, theDocumentSession, theContext);
    }

    [Fact]
    public void the_account_balance_should_be_negative()
    {
        theAccount.Balance.ShouldBe(-200);
    }

    [Fact]
    public void raises_an_account_overdrawn_message()
    {
        // ShouldHaveMessageOfType() is an extension method in
        // Wolverine itself to facilitate unit testing assertions like this
        theContext.Sent.ShouldHaveMessageOfType<AccountOverdrawn>()
            .AccountId.ShouldBe(theAccount.Id);
    }

    [Fact]
    public void raises_an_overdrawn_deadline_message_in_10_days()
    {
        theContext.ScheduledMessages()
            // Find the wrapping envelope for this message type,
            // then we can chain assertions against the wrapping Envelope
            .ShouldHaveEnvelopeForMessageType<EnforceAccountOverdrawnDeadline>()
            .ScheduleDelay.ShouldBe(10.Days());
    }

    public Task DisposeAsync()
    {
        return Task.CompletedTask;
    }
}

snippet source | anchor

The TestMessageContext mostly just collects an array of objects that are sent, published, or scheduled. The same extension methods explained in the previous section can be used to verify the outgoing messages and even how they were published.

As of Wolverine 1.8, TestMessageContext also supports limited expectations for request and reply using IMessageBus.InvokeAsync<T>() as shown below:

cs
var spy = new TestMessageContext();
var context = (IMessageContext)spy;

// Set up an expected response for a message
spy.WhenInvokedMessageOf<NumberRequest>()
    .RespondWith(new NumberResponse(12));

// Used for:
var response1 = await context.InvokeAsync<NumberResponse>(new NumberRequest(4, 5));

// Set up an expected response with a matching filter
spy.WhenInvokedMessageOf<NumberRequest>(x => x.X == 4)
    .RespondWith(new NumberResponse(12));

// Set up an expected response for a message to an explicit destination Uri
spy.WhenInvokedMessageOf<NumberRequest>(destination:new Uri("rabbitmq://queue/incoming"))
    .RespondWith(new NumberResponse(12));

// Used to set up:
var response2 = await context.EndpointFor(new Uri("rabbitmq://queue/incoming"))
    .InvokeAsync<NumberResponse>(new NumberRequest(5, 6));

// Set up an expected response for a message to a named endpoint
spy.WhenInvokedMessageOf<NumberRequest>(endpointName:"incoming")
    .RespondWith(new NumberResponse(12));

// Used to set up:
var response3 = await context.EndpointFor("incoming")
    .InvokeAsync<NumberResponse>(new NumberRequest(5, 6));

snippet source | anchor

Stubbing All External Transports

TIP

In all cases here, Wolverine is disabling all external listeners, stubbing all outgoing subscriber endpoints, and not making any connection to external brokers.

Unlike some older .NET messaging tools, Wolverine comes out of the box with its in-memory "mediator" functionality that allows you to directly invoke any possible message handler in the system on demand without any explicit configuration. Great, and that means that there's value in just spinning up the application as is and executing locally -- but what about any external transport dependencies that may be very inconvenient to utilize in automated tests?

To that end, Wolverine allows you to completely disable all external transports including the built in TCP transport. There's a couple different ways to go about it. The simplest conceptual approach is to leverage the .NET environment name like this:

cs
var builder = Host.CreateApplicationBuilder();
builder.UseWolverine(opts =>
{
    // Other configuration...

    // IF the environment is "Testing", turn off all external transports
    if (builder.Environment.IsDevelopment())
    {
        opts.StubAllExternalTransports();
    }
});

using var host = builder.Build();
await host.StartAsync();

snippet source | anchor

I'm not necessarily comfortable with a lot of conditional hosting setup all the time, so there's another option to use the IServiceCollection.DisableAllExternalWolverineTransports() extension method as shown below:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // do whatever you need to configure Wolverine
    })

    // Override the Wolverine configuration to disable all
    // external transports, broker connectivity, and incoming/outgoing
    // messages to run completely locally
    .ConfigureServices(services => services.DisableAllExternalWolverineTransports())

    .StartAsync();

snippet source | anchor

Finally, to put that in a little more context about how you might go about using it in real life, let's say that we have out main application with a relatively clean bootstrapping setup and a separate integration testing project. In this case we'd like to bootstrap the application from the integration testing project as it is, except for having all the external transports disabled. In the code below, I'm using the Alba and WebApplicationFactory:

cs
// This is using Alba to bootstrap a Wolverine application
// for integration tests, but it's using WebApplicationFactory
// to do the actual bootstrapping
await using var host = await AlbaHost.For<Program>(x =>
{
    // I'm overriding
    x.ConfigureServices(services => services.DisableAllExternalWolverineTransports());
});

snippet source | anchor

In the sample above, I'm bootstrapping the IHost for my production application with all the external transports turned off in a way that's appropriate for integration testing message handlers within the main application.

Running Wolverine in "Solo" Mode 3.0

Wolverine's leadership election process is necessary for distributing several background tasks in real life production, but that subsystem can lead to some inconvenient sluggishness in cold start times in automation testing.

To sidestep that problem, you can direct Wolverine to run in "Solo" mode where the current process assumes that it's the only running node and automatically starts up all known background tasks immediately.

To do so, you could do something like this in your main Program file:

cs
var builder = Host.CreateApplicationBuilder();

builder.UseWolverine(opts =>
{
    opts.Services.AddMarten("some connection string")

        // This adds quite a bit of middleware for
        // Marten
        .IntegrateWithWolverine();

    // You want this maybe!
    opts.Policies.AutoApplyTransactions();

    if (builder.Environment.IsDevelopment())
    {
        // But wait! Optimize Wolverine for usage as
        // if there would never be more than one node running
        opts.Durability.Mode = DurabilityMode.Solo;
    }
});

using var host = builder.Build();
await host.StartAsync();

snippet source | anchor

Or if you're using something like WebHostFactory to bootstrap your Wolverine application in an integration testing harness, you can use this helper to override Wolverine into being "Solo":

cs
// This is bootstrapping the actual application using
// its implied Program.Main() set up
// For non-Alba users, this is using IWebHostBuilder 
Host = await AlbaHost.For<Program>(x =>
{
    x.ConfigureServices(services =>
    {
        // Override the Wolverine configuration in the application
        // to run the application in "solo" mode for faster
        // testing cold starts
        services.RunWolverineInSoloMode();

        // And just for completion, disable all Wolverine external 
        // messaging transports
        services.DisableAllExternalWolverineTransports();
    });
});

snippet source | anchor

Stubbing Message Handlers 5.1

To extend the test automation support even further, Wolverine now has a capability to "stub" out message handlers in testing scenarios with pre-canned behavior for more reliable testing in some situations. This feature was mostly conceived of for stubbing out calls to external systems through IMessageBus.InvokeAsync<T>() where the request would normally be sent to an external system through a subscriber.

Jumping into an example, let's say that your system interacts with another service that estimates delivery costs for ordering items. At some point in the system you might reach out through a request/reply call in Wolverine to estimate an item delivery before making a purchase like this code:

cs
// This query message is normally sent to an external system through Wolverine
// messaging
public record EstimateDelivery(int ItemId, DateOnly Date, string PostalCode);

// This message type is a response from an external system
public record DeliveryInformation(TimeOnly DeliveryTime, decimal Cost);

public record MaybePurchaseItem(int ItemId, Guid LocationId, DateOnly Date, string PostalCode, decimal BudgetedCost);
public record MakePurchase(int ItemId, Guid LocationId, DateOnly Date);
public record PurchaseRejected(int ItemId, Guid LocationId, DateOnly Date);

public static class MaybePurchaseHandler
{
    public static Task<DeliveryInformation> LoadAsync(
        MaybePurchaseItem command, 
        IMessageBus bus, 
        CancellationToken cancellation)
    {
        var (itemId, _, date, postalCode, budget) = command;
        var estimateDelivery = new EstimateDelivery(itemId, date, postalCode);
        
        // Let's say this is doing a remote request and reply to another system
        // through Wolverine messaging
        return bus.InvokeAsync<DeliveryInformation>(estimateDelivery, cancellation);
    }
    
    public static object Handle(
        MaybePurchaseItem command, 
        DeliveryInformation estimate)
    {

        if (estimate.Cost <= command.BudgetedCost)
        {
            return new MakePurchase(command.ItemId, command.LocationId, command.Date);
        }

        return new PurchaseRejected(command.ItemId, command.LocationId, command.Date);
    }
}

snippet source | anchor

And for a little more context, the EstimateDelivery message will always be sent to an external system in this configuration:

cs
var builder = Host.CreateApplicationBuilder();
builder.UseWolverine(opts =>
{
    opts
        .UseRabbitMq(builder.Configuration.GetConnectionString("rabbit"))
        .AutoProvision();

    // Just showing that EstimateDelivery is handled by
    // whatever system is on the other end of the "estimates" queue
    opts.PublishMessage<EstimateDelivery>()
        .ToRabbitQueue("estimates");
});

snippet source | anchor

Using our

In testing scenarios, maybe the external system isn't available at all, or it's just much more challenging to run tests that also include the external system, or maybe you'd just like to write more isolated tests against your service's behavior before even trying to integrate with the other system (my personal preference anyway). To that end we can now stub the remote handling like this:

cs
public static async Task try_application(IHost host)
{
    host.StubWolverineMessageHandling<EstimateDelivery, DeliveryInformation>(
        query => new DeliveryInformation(new TimeOnly(17, 0), 1000));

    var locationId = Guid.NewGuid();
    var itemId = 111;
    var expectedDate = new DateOnly(2025, 12, 1);
    var postalCode = "78750";

    var maybePurchaseItem = new MaybePurchaseItem(itemId, locationId, expectedDate, postalCode,
        500);
    
    var tracked =
        await host.InvokeMessageAndWaitAsync(maybePurchaseItem);
    
    // The estimated cost from the stub was more than we budgeted
    // so this message should have been published
    
    // This line is an assertion too that there was a single message
    // of this type published as part of the message handling above
    var rejected = tracked.Sent.SingleMessage<PurchaseRejected>();
    rejected.ItemId.ShouldBe(itemId);
    rejected.LocationId.ShouldBe(locationId);
}

snippet source | anchor

After calling making this call:

csharp
        host.StubWolverineMessageHandling<EstimateDelivery, DeliveryInformation>(
            query => new DeliveryInformation(new TimeOnly(17, 0), 1000));

Calling this from our Wolverine application:

csharp
        // Let's say this is doing a remote request and reply to another system
        // through Wolverine messaging
        return bus.InvokeAsync<DeliveryInformation>(estimateDelivery, cancellation);

Will use the stubbed logic we registered. This is enabling you to use fake behavior for difficult to use external services.

For the next test, we can completely remove the stub behavior and revert back to the original configuration like this:

cs
public static void revert_stub(IHost host)
{
    // Selectively clear out the stub behavior for only one message
    // type
    host.WolverineStubs(stubs =>
    {
        stubs.Clear<EstimateDelivery>();
    });
    
    // Or just clear out all active Wolverine message handler
    // stubs
    host.ClearAllWolverineStubs();
}

snippet source | anchor

Or instead, we can just completely replace the previously registered stub behavior with completely new logic that will override our previous stub:

cs
public static void override_stub(IHost host)
{
    host.StubWolverineMessageHandling<EstimateDelivery, DeliveryInformation>(
        query => new DeliveryInformation(new TimeOnly(17, 0), 250));

}

snippet source | anchor

So far, we've only looked at simple request/reply behavior, but what if a remote system receiving our message potentially makes multiple calls back to our system? Or really just any kind of interaction more complicated than a single response for a request message?

We're still in business, we just have to use a little uglier signature for our stub:

cs
public static void more_complex_stub(IHost host)
{
    host.WolverineStubs(stubs =>
    {
        stubs.Stub<EstimateDelivery>(async (
            EstimateDelivery message, 
            IMessageContext context, 
            IServiceProvider services,
            CancellationToken cancellation) =>
        {
            // do whatever you want, including publishing any number of messages
            // back through IMessageContext
            
            // And grab any other services you might need from the application 
            // through the IServiceProvider -- but note that you will have
            // to deal with scopes yourself here

            // This is an equivalent to get the response back to the 
            // original caller
            await context.PublishAsync(new DeliveryInformation(new TimeOnly(17, 0), 250));
        });
    });
}

snippet source | anchor

A few notes about this capability:

  • You can use any number of stubs for different message types at the same time
  • Most of the testing samples use extension methods on IHost, but we know there are some users who bootstrap only an IoC container for integration tests, so all of the extension methods shown in this section are also available off of IServiceProvider
  • The "stub" functions are effectively singletons. There's nothing fancier about argument matching or anything you might expect from a full fledged mock library like NSubstitute or FakeItEasy
  • You can actually fake out the routing to message types that are normally handled by handlers within the application
  • We don't believe this feature will be helpful for "sticky" message handlers where you may have multiple handlers for the same message type interally

Released under the MIT License.