How Wolverine allows for easier testing

Yesterday I blogged about the new Wolverine alpha release with a sample that hopefully showed off how Wolverine’s different approach can lead to better developer productivity and higher performance than similar tools. Today I want to follow up on that by extending the code sample with other functionality, but then diving into how Wolverine (hopefully) makes automated unit or integration testing easier than what you may be used to.

From yesterday’s sample, I showed this small message handler for applying a debit to a bank account from an incoming message:

public static class DebitAccountHandler
{
    [Transactional] 
    public static void Handle(DebitAccount command, Account account, IDocumentSession session)
    {
        account.Balance -= command.Amount;
        session.Store(account);
    }
}

Today let’s extend this to:

  1. Raise an event if the balance gets below a specified threshold for the account
  2. Or raises a different event if the balance goes negative, but also…
  3. Sends a second “timeout” message to carry out some kind of enforcement action against the account if it is still negative by then

Here’s the new event and command messages:

public record LowBalanceDetected(Guid AccountId) : IAccountCommand;
public record AccountOverdrawn(Guid AccountId) : IAccountCommand;

// We'll change this in a little bit
public class EnforceAccountOverdrawnDeadline : IAccountCommand
{
    public Guid AccountId { get; }

    public EnforceAccountOverdrawnDeadline(Guid accountId)
    {
        AccountId = accountId;
    }
}

Now, we could extend the message handler to raise the necessary events and the overdrawn enforcement command message like so:

    [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);

        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));
            
            // Give the customer 10 days to deal with the overdrawn account
            await messaging.ScheduleAsync(new EnforceAccountOverdrawnDeadline(account.Id), 10.Days());
        }
    }

And just to add a little more context, here’s part of what the message handler for the EnforceAccountOverdrawnDeadline could look like:

    public static void Handle(EnforceAccountOverdrawnDeadline command, Account account)
    {
        // Don't do anything if the balance has been corrected
        if (account.Balance >= 0) return;
        
        // Dunno, send in the goons? Report them to a credit agency? Guessing
        // nothing pleasant happens here
    }

Alrighty then, back to the new version of the message handler that raises extra event messages depending on the state of the account. You’ll notice that I used method injection to pass in the Wolverine IMessageContext for the current message being handled. That gives me access to spawn additional messages and even schedule the execution of a command for a later time. You should notice that I now had to make the handler method asynchronous as the various SendAsync() calls return ValueTask, so it’s a little uglier now. Don’t worry, we’re going to come back to that, so don’t settle for this quite yet.

I’m going to leave this for the next post, but if you’re experienced with asynchronous messaging you’re screaming that there’s a potential race condition or risk of phantom data or messages between the extra messages going out and the Account being committed. Tomorrow I’ll discuss how Wolverine’s transactional outbox support removes those very real, very common problems in asynchronous message processing.

So let’s jump into what a unit test could look like for the message handler for the DebitAccount method. To start with, I’ll use Wolverine’s built in TestMessageContext to act as a “spy” on the method. A couple tests might look like this using my typical testing stack of xUnit.Net, Shouldly, and NSubstitute:

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()
    {
        var scheduledTime  = theContext.ScheduledMessages()
            // Also an extension method in Wolverine for testing
            .ShouldHaveEnvelopeForMessageType<EnforceAccountOverdrawnDeadline>()
            .ScheduledTime;
        
        // Um, do something to verify that the scheduled time is 10 days from this moment
        // and also:
        //  https://github.com/JasperFx/wolverine/issues/110
    }

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

It’s not horrendous, and I’ve seen much, much worse in real life code. All the same though, let’s aim for easier code to test by removing more infrastructure code and trying to get to purely synchronous code. To get there, I’m first going to start with the EnforceAccountOverdrawnDeadline message type and change it slightly to this:

// I'm hard coding the delay time for execution, just
// go with that for now please:)
public record EnforceAccountOverdrawnDeadline(Guid AccountId) : TimeoutMessage(10.Days()), IAccountCommand;

And now back the the Handle(DebitAccount) handler, we’ll use Wolverine’s concept of cascading messages to simplify the handler and make it completely synchronous:

    [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);

        if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
        {
            yield return new LowBalanceDetected(account.Id);
        }
        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);
        }
    }

Now, we’re able to mostly use state-based testing, eliminate the fake IMessageContext, and work with strictly synchronous code. Here’s the new version of the test class from before:

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

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

    public when_the_account_is_overdrawn()
    {
        var command = new DebitAccount(theAccount.Id, 1200);
        theOutboundMessages = DebitAccountHandler.Handle(command, theAccount, theDocumentSession)
            .ToArray();
    }

    [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
        theOutboundMessages.ShouldHaveMessageOfType<AccountOverdrawn>()
            .AccountId.ShouldBe(theAccount.Id);
    }

    [Fact]
    public void raises_an_overdrawn_deadline_message_in_10_days()
    {
        var scheduledTime  = theOutboundMessages
            // Also an extension method in Wolverine for testing
            .ShouldHaveEnvelopeForMessageType<EnforceAccountOverdrawnDeadline>();
    }

    [Fact]
    public void should_not_raise_account_balance_low_event()
    {
        theOutboundMessages.ShouldHaveNoMessageOfType<LowBalanceDetected>();
    }
}

The second version of both the handler method and the accompanying unit test is arguably simpler because:

  1. We were able to make the handler method synchronous which helpfully removes some boilerplate code, which is especially helpful if you use xUnit.Net because that allows us to eschew the IAsyncLifetime thing.
  2. Except for verifying that the account data was stored, all of the unit test code is now using state-based testing, which is generally easier to understand and write than interaction-based tests that necessarily depend on mock objects

Wolverine in general also made the handler method easier to test through the middleware I introduced in my previous post that “pushes” in the Account data to the handler method instead of making you jump through data access code and potential mock/stub object setup to inject the data inputs.

At the end of the day, I think that Wolverine not only does quite a bit to simplify your actual application code by doing more to isolate business functionality away from infrastructure, Wolverine also leads to more easily testable code for effective Test Driven Development.

But what about……….?

I meant to also show Wolverine’s built in integration testing support, but to be honest, I’m about to meet a friend for lunch and I’ve gotta wrap this up in the next 10 minutes. In subsequent posts I’m going to stick with this example and extend that into integration testing across the original message and into the cascading messages. I’ll also get into the very important details about Wolverine’s transactional outbox support.

Advertisement

2 thoughts on “How Wolverine allows for easier testing

  1. Thanks for the post.

    Quick question – Is a sync handler, just that, a sync operation? Or is there something going on in the background making the operation async? Do you have any concerns regarding scalability?

    1. Hey Marcio, good question.

      Look at the previous post at https://jeremydmiller.com/2022/12/12/introducing-wolverine-for-effective-server-side-net-development/ and search for `DebitAccountHandler1928499868` and I think that’ll answer your question. Wolverine is generating code around the handler code and any middleware to its required adapter interface, and *that* signature is absolutely asynchronous. At no point is there any IO happening in synchronous methods, the `IDocumentSession.Store()` call just tags the account document as needing to be persisted, and there’s no IO happening at that point.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s