Event-driven .NET: How to unit test code that depends on an event

Testing code with a dependency on an event can be tricky. You may be tempted to directly unit test the event handling method. However, that doesn’t prove that code under test is listening for the event. It’s better to really fire the event, and then test side effects of the event handling method.

Firing the event isn’t simple though. The key problem is you can’t fire the event from outside the class. It must be invoked within the class that contains the event.

In this article, I’ll show how to write a unit test against code that depends on an event.

The code under test – MessageProcessor and its dependencies

I have a class called MessageProcessor. Its job is to process newly received messages and save them in the repository.

public class MessageProcessor { private readonly IMessageGetter MessageGetter; private readonly IMessageRepository MessageRepository; public MessageProcessor(IMessageGetter messageGetter, IMessageRepository messageRepository) { MessageGetter = messageGetter; MessageRepository = messageRepository; } }
Code language: C# (cs)

MessageProcessor depends on two interfaces: IMessageGetter and IMessageRepository. Imagine the message getter looks for external messages. When it detects new messages, it fires the NewMessage event.

public delegate void NewMessageEventHandler(string message); public interface IMessageGetter { event NewMessageEventHandler NewMessage; } public interface IMessageRepository { void Save(string message); }
Code language: C# (cs)

Write a unit test that fires the event and checks the side effects

I’m going to write a test that proves MessageProcessor listens for new messages and saves them to the repository. In other words, I’m testing the side effects of the event firing.

To test this, I need to mock out IMessageGetter and IMessageRepository and pass them to MessageProcessor. To actually fire the NewMessage event, I need to call Raise() on the IMessageGetter mock.

Note: This is using Moq.

[TestMethod()] public void WhenNewMessageEventFires_ThenMessageProcessorSavesMessage() { //arrange var mockMessageGetter = new Mock<IMessageGetter>(); var mockRepo = new Mock<IMessageRepository>(); var messageProcessor = new MessageProcessor(mockMessageGetter.Object, mockRepo.Object); string message = "abc"; //act mockMessageGetter.Raise(t => t.NewMessage += null, message); //assert mockRepo.Verify(t => t.Save(message), Times.Once()); }
Code language: C# (cs)

The Raise() method has strange syntax. This is why at the end of this article, I’ll show a simpler approach that’s easier to understand.

When I run the test, it fails as expected:

Test method MessageSystem.Tests.MessageProcessorTests.WhenNewMessageEventFires_ThenMessageProcessor_SavesReversedMessage threw exception: Moq.MockException: Expected invocation on the mock once, but was 0 times: t => t.Save("abc")
Code language: plaintext (plaintext)

Write event handling code that makes the test pass

Now that I have the unit test in place, I need to write code that passes the test.

First, I need to wire up the event handler. This is why it’s a good idea to write tests that actually fire the event. It forced me to wire up the event handler.

Then I need to implement the event handling code – which simply calls Save() on the repository object.

public class MessageProcessor { private readonly IMessageGetter MessageGetter; private readonly IMessageRepository MessageRepository; public MessageProcessor(IMessageGetter messageGetter, IMessageRepository messageRepository) { MessageGetter = messageGetter; MessageRepository = messageRepository; MessageGetter.NewMessage += OnNewMessage; } private void OnNewMessage(string message) { MessageRepository.Save(message); } }
Code language: C# (cs)

The unit test now passes.

Using a manual mock to simplify the test

It’s a good idea to make sure your tests are easy to understand. Clean code, clean tests. The syntax of Moq Raise() is difficult to understand, and there’s a simple alternative: use a manual mock instead.

public class MockMessageGetter : IMessageGetter { public void InvokeEvent(string message) { NewMessage?.Invoke(message); } public event NewMessageEventHandler NewMessage; }
Code language: C# (cs)

What’s the InvokeEvent() method for? Remember the key problem: you can’t invoke an event from outside the class. So I had to add this method that simply invokes the event.

Now I simply need to use MockMessageGetter in the unit test:

[TestClass()] public class MessageProcessorTests { [TestMethod()] public void WhenNewMessageEventFires_ThenMessageProcessor_SavesReversedMessage() { //arrange var mockMessageGetter = new MockMessageGetter(); var mockRepo = new Mock<IMessageRepository>(); var messageProcessor = new MessageProcessor(mockMessageGetter, mockRepo.Object); string message = "abc"; //act mockMessageGetter.InvokeEvent(message); //assert mockRepo.Verify(t => t.Save(message), Times.Once()); } }
Code language: JavaScript (javascript)

Easier to understand than Moq Raise(), right?

Leave a Comment