In this lesson, you will learn how to design testable event handlers by decoupling your producers from your consumers, ensuring your logic is verifiable without needing a live message broker.
The Challenge of Tight Coupling
In many event-driven systems, developers instantiate an event bus (like RabbitMQ or Azure Service Bus) directly inside their handlers. This forces your unit tests to either spin up complex infrastructure or rely on fragile mocks of the underlying client libraries. To make your system testable, you must treat the event bus as an infrastructure detail—an "adapter"—that sits behind an interface.
Decoupling via Ports and Adapters
By applying the principles of Ports and Adapters, you define an interface (the "Port") that represents the capability to publish an event. Your application logic depends only on this interface. When testing, you can inject a simple in-memory implementation rather than a real broker.
Consider this decoupled approach:
// The Port
public interface IEventPublisher {
Task PublishAsync<T>(T @event);
}
// The Handler (Testable because it uses the interface)
public class OrderProcessor {
private readonly IEventPublisher _publisher;
public OrderProcessor(IEventPublisher publisher) => _publisher = publisher;
public async Task Process(Order order) {
// Business logic...
await _publisher.PublishAsync(new OrderPlacedEvent(order.Id));
}
}
Verifying Behavior in Tests
Because OrderProcessor consumes IEventPublisher via dependency injection, testing becomes trivial. You can use a "Spy" or a simple FakeEventPublisher to verify that the correct event was dispatched without ever touching a network socket:
[Fact]
public async Task Process_ShouldPublishOrderPlacedEvent() {
var spy = new FakeEventPublisher();
var sut = new OrderProcessor(spy);
await sut.Process(new Order());
Assert.Contains(spy.PublishedEvents, e => e is OrderPlacedEvent);
}
By decoupling the producer's intent to publish from the infrastructure's mechanism of delivery, you transform event-driven testing from an integration nightmare into a fast, reliable unit testing process.
Key Takeaway: Decouple your domain logic from the message broker by injecting an abstraction, allowing you to verify event emissions using simple in-memory test doubles instead of infrastructure dependencies.