Search code examples
c#unit-testingxunitmediatrmediator

Unit testing Mediator published expected INotification


I have a Blazor server side application that is using events to trigger updates between clients. I have decided to migrate this to instead use MediatR to trigger updates. The actual implementation is working, but I'm kind of stuck on the unit tests. Previously I used FluentAssertions with IMonitor to verify an event was raised. Now I wanna do something similar with MediatR. I've seen questions related to this on SO, with people answering "you shouldn't test the internals of MediatR". In my case I'm sure I'm not testing the internals of MediatR. What I want to test is that a notification was actually published, with the expected values.

First I'll show my working implementation with events. The important parts here is a service that does some business logic and fires an event if everything looks good, a class UpdateNotifier that exposes the event and methods for firing them, and lastly there's a unit test that tests the service.

public class MessageService
{
    private readonly IUpdateNotifier _notifier;

    public MessageService(IUpdateNotifier notifier)
    {
        _notifier = notifier;
    }

    public async Task<bool> PublishMessage(DateTimeOffset timestamp, string username, string message)
    {
        if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(message))
        {
            return false;
        }

        _notifier.FireMessagePublished(timestamp, username, message);
        return true;
    }
}

public class UpdateNotifier : IUpdateNotifier
{
    public event EventHandler<MessagePublishedEventArgs>? MessagePublished;

    public void FireMessagePublished(DateTimeOffset timestamp, string username, string message)
    {
        MessagePublished?.Invoke(this, new MessagePublishedEventArgs(timestamp, username, message));
    }
}

public class MessagePublishedEventArgs
{
    public MessagePublishedEventArgs(DateTimeOffset timestamp, string username, string message)
    {
        Timestamp = timestamp;
        Username = username;
        Message = message;
    }

    public DateTimeOffset Timestamp { get; init; }
    public string Username { get; init; }
    public string Message { get; init; }
}

public class MessageServiceTests
{
    private readonly IUpdateNotifier _notifier;
    private readonly MessageService _messageService;

    public MessageServiceTests()
    {
        _notifier = new UpdateNotifier();
        _messageService = new MessageService(_notifier);
    }

    [Fact]
    public async Task PublishMessage_MessagePublished_HasExpectedValues()
    {
        // Arrange
        var timestamp = DateTimeOffset.Now;
        var username = "Eric";
        var message = "Hi! :)";
        using var updateNotifierMonitor = _notifier.Monitor();

        // Act
        var publishMessageResult = await _messageService.PublishMessage(timestamp, username, message);

        // Assert
        publishMessageResult.Should().BeTrue();
        updateNotifierMonitor.Should()
            .Raise(nameof(_notifier.MessagePublished))
            .WithArgs<MessagePublishedEventArgs>(args => args.Timestamp.Equals(timestamp) && args.Username.Equals(username) && args.Message.Equals(message));
    }
}

In my migration, what I've changed is to remove the IUpdateNotifier/UpdateNotifier. MessageService relies on IMediator instead. And instead of MessagePublishedEventArgs there's a MessagePublishedNotification that implements INotification.

public async Task<bool> PublishMessage(DateTimeOffset timestamp, string username, string message)
{
    if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(message))
    {
        return false;
    }

    await _mediator.Publish(new MessagePublishedNotification(timestamp, username, message));
    return true;
}

Is there any way to write a unit test in the same way as my original unit test, but that verifies an INotification of expected type with expected values was published?

I don't see any reason to mock anything here. I want to use the actual implementation for everything.


Solution

  • I've figured out how to solve this according to my specifications.

    In the test project, implement a generic INotificationHandler that can subscribe to any notifications:

    public class TestNotificationHandler<TNotification> : INotificationHandler<TNotification>, IDisposable
        where TNotification : INotification
    {
        internal List<TNotification> Notifications { get; } = new();
    
        public async Task Handle(TNotification notification, CancellationToken cancellationToken)
        {
            Notifications.Add(notification);
            await Task.CompletedTask;
        }
    
        public void Dispose()
        {
            Notifications.Clear();
        }
    }
    

    Update MessageServiceTests with the following changes:

    1. Add support for MediatR
    2. Instantiate a TestNotificationHandler with the notification we want to subscribe to (in this case, MessagePublishedNotification)
    3. Implement IDisposable so that the messages are cleared between unit tests
    4. Update the unit test to verify that TestNotificationHandler received the notification
    public class MessageServiceTests : IDisposable
    {
        private readonly MessageService _messageService;
        private readonly TestNotificationHandler<MessagePublishedNotification> _notificationHandler;
    
        public MessageServiceTests()
        {
            var services = new ServiceCollection();
            services.AddMediator();
            _notificationHandler = new TestNotificationHandler<MessagePublishedNotification>();
            services.AddTransient<INotificationHandler<MessagePublishedNotification>>(_ => _notificationHandler);
            _messageService = new MessageService(services.BuildServiceProvider().GetRequiredService<IMediator>());
        }
        
        [Fact]
        public async Task PublishMessage_MessagePublished_HasExpectedValues()
        {
            // Arrange
            var timestamp = DateTimeOffset.Now;
            var username = "Eric";
            var message = "Hi! :)";
    
            // Act
            var publishMessageResult = await _messageService.PublishMessage(timestamp, username, message);
    
            // Assert
            publishMessageResult.Should().BeTrue();
            _notificationHandler.Notifications.Should()
                .Contain(n => n.Timestamp == timestamp && n.Username == username && n.Message == message);
        }
    
        public void Dispose()
        {
            _notificationHandler.Dispose();
        }
    }
    

    Edit:

    During my rather extensive testing with this approach, I've found two ways in which this can break. So please take care to adhere to these notes:

    1. It's important that services.BuildServiceProvider().GetRequiredService<IMediator>() in the test setup is called after adding all the notification handlers. Otherwise they don't get called.
    2. In one case I forgot to implement IDisposable on my handler. Somehow that broke the implementation. When I debugged my handler was called, but it appeared to have created two instances of my handler since when the test was asserting there were no notifications in the list. I haven't been able to reproduce this consistently.