Skip to content

Testing LiteBus

A. Shafie edited this page Sep 26, 2025 · 1 revision

Testing LiteBus

Testing is a first-class concern in applications built with LiteBus. The library's design, which emphasizes separation of concerns and dependency injection, makes it highly testable at different levels. This guide covers strategies for unit testing handlers and integration testing the full mediation pipeline.

Unit Testing Handlers

Handlers are simple classes with dependencies, making them easy to unit test in isolation. You do not need the LiteBus infrastructure to test a handler's logic.

Example: Unit Testing a Command Handler

Given a command handler that creates a product:

public sealed class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, Guid>
{
    private readonly IProductRepository _repository;

    public CreateProductCommandHandler(IProductRepository repository)
    {
        _repository = repository;
    }

    public async Task<Guid> HandleAsync(CreateProductCommand command, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(command.Name))
        {
            throw new ValidationException("Product name is required.");
        }

        var product = new Product(command.Name);
        await _repository.AddAsync(product, cancellationToken);
        return product.Id;
    }
}

You can test its logic using a mocking framework like Moq:

[Fact]
public async Task CreateProductCommandHandler_WithValidName_ShouldAddProductToRepository()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var handler = new CreateProductCommandHandler(mockRepository.Object);
    var command = new CreateProductCommand { Name = "Laptop" };

    // Act
    var productId = await handler.HandleAsync(command, CancellationToken.None);

    // Assert
    Assert.NotEqual(Guid.Empty, productId);
    mockRepository.Verify(r => r.AddAsync(It.Is<Product>(p => p.Name == "Laptop"), It.IsAny<CancellationToken>()), Times.Once);
}

[Fact]
public async Task CreateProductCommandHandler_WithEmptyName_ShouldThrowValidationException()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var handler = new CreateProductCommandHandler(mockRepository.Object);
    var command = new CreateProductCommand { Name = "" };

    // Act & Assert
    await Assert.ThrowsAsync<ValidationException>(() => handler.HandleAsync(command, CancellationToken.None));
}

This same approach applies to pre-handlers, post-handlers, and error handlers.

Integration Testing the Full Pipeline

To test the entire mediation pipeline, including pre-handlers, the main handler, and post-handlers, you can use an in-memory test setup.

The LiteBus.Testing package provides a LiteBusTestBase class that automatically clears the static MessageRegistry between tests to ensure test isolation.

Example: Integration Testing a Command Pipeline

Consider a pipeline with a validator (pre-handler) and a notifier (post-handler).

public class CommandIntegrationTests : LiteBusTestBase // Inherit for test isolation
{
    private readonly IServiceProvider _serviceProvider;

    public CommandIntegrationTests()
    {
        var services = new ServiceCollection();

        // Register mock dependencies
        services.AddSingleton<IProductRepository, InMemoryProductRepository>();
        services.AddSingleton(new Mock<IEventPublisher>().Object); // Mock for the post-handler

        // Configure LiteBus with all relevant handlers
        services.AddLiteBus(liteBus =>
        {
            liteBus.AddCommandModule(module =>
            {
                module.Register<CreateProductCommandValidator>(); // Pre-handler
                module.Register<CreateProductCommandHandler>();   // Main handler
                module.Register<ProductCreationNotifier>();     // Post-handler
            });
        });

        _serviceProvider = services.BuildServiceProvider();
    }

    [Fact]
    public async Task SendAsync_WithValidCommand_ShouldExecuteFullPipeline()
    {
        // Arrange
        var commandMediator = _serviceProvider.GetRequiredService<ICommandMediator>();
        var mockEventPublisher = _serviceProvider.GetRequiredService<Mock<IEventPublisher>>();
        var command = new CreateProductCommand { Name = "Test Product" };

        // Act
        var productId = await commandMediator.SendAsync(command);

        // Assert
        // 1. Check the result from the main handler
        Assert.NotEqual(Guid.Empty, productId);

        // 2. Verify the repository was called (by the main handler)
        var repository = _serviceProvider.GetRequiredService<IProductRepository>();
        var product = await repository.GetByIdAsync(productId);
        Assert.NotNull(product);

        // 3. Verify the notifier was called (by the post-handler)
        mockEventPublisher.Verify(p => p.PublishAsync(It.IsAny<ProductCreatedEvent>(), null, It.IsAny<CancellationToken>()), Times.Once);
    }
}

Testing Code that Uses Mediators

When testing a class that depends on a mediator (e.g., a controller), you can mock the mediator interface to isolate the class from the LiteBus pipeline.

[Fact]
public async Task ProductsController_Create_ShouldReturnCreatedResult()
{
    // Arrange
    var mockMediator = new Mock<ICommandMediator>();
    var command = new CreateProductCommand { Name = "New Gadget" };
    var expectedProductId = Guid.NewGuid();

    mockMediator
        .Setup(m => m.SendAsync(command, null, It.IsAny<CancellationToken>()))
        .ReturnsAsync(expectedProductId);

    var controller = new ProductsController(mockMediator.Object);

    // Act
    var result = await controller.Create(command);

    // Assert
    var createdResult = Assert.IsType<CreatedAtActionResult>(result.Result);
    Assert.Equal(expectedProductId, createdResult.RouteValues["id"]);
}

Best Practices

  1. Unit Test Handlers in Isolation: The majority of your business logic lives in handlers. Focus your testing efforts here with simple unit tests.
  2. Use Mocks for Dependencies: When unit testing a handler, mock its dependencies (repositories, services, etc.) to ensure you are only testing the handler's logic.
  3. Integration Test Key Pipelines: Write a few integration tests for your most critical command/query pipelines to ensure that pre-handlers, main handlers, and post-handlers work together correctly.
  4. Use an In-Memory Database: For integration tests involving repositories, use an in-memory database provider (like EF Core's InMemory provider or a real database in a test container) to ensure tests are fast and isolated.
  5. Inherit from LiteBusTestBase: When writing tests that use the full LiteBus pipeline, inherit from LiteBusTestBase to prevent state from leaking between tests.
Clone this wiki locally