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.
I've figured out how to solve this according to my specifications.
In the test project, implement a generic
INotificationHandlerthat can subscribe to any notifications:Update
MessageServiceTestswith the following changes:TestNotificationHandlerwith the notification we want to subscribe to (in this case,MessagePublishedNotification)IDisposableso that the messages are cleared between unit testsTestNotificationHandlerreceived the notificationEdit:
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:
services.BuildServiceProvider().GetRequiredService<IMediator>()in the test setup is called after adding all the notification handlers. Otherwise they don't get called.IDisposableon 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.