Course contentsShow
← C#
Lesson 1715 of 1,90815. Software Quality and TestingFree lesson

Modular Monolith Testing

Define boundaries that allow testing internal modules in isolation.

In this lesson, you will learn how to structure your Modular Monolith boundaries so you can test individual business modules in isolation without dragging the entire application into memory.

The Problem of "Shared Everything"

In a typical monolith, components are tightly coupled. If you try to test your Orders module, you might find yourself needing the Inventory, Shipping, and Identity modules just to satisfy constructor dependencies. In a Modular Monolith, we treat these boundaries as strict contracts. To test a module in isolation, we must expose only its public entry points (often via a PublicApi or Service interface) while keeping internal implementation details hidden.

Establishing the Boundary

To test effectively, your test project should reference the "Module Interface" layer, but not the concrete implementations of other modules. If Orders needs to check Inventory, we inject an IInventoryService rather than a concrete InventoryService.

By using a Module Bootstrapper pattern, we can initialize only the necessary module during a test run:

public class OrderModuleTests
{
    private readonly IOrderService _orderService;

    public OrderModuleTests()
    {
        // We only spin up the dependencies required by this module
        var services = new ServiceCollection();
        services.AddOrderModule();
        
        // Use a mock or an in-memory test implementation for cross-module needs
        services.AddSingleton<IInventoryService>(new MockInventoryService());
        
        var provider = services.BuildServiceProvider();
        _orderService = provider.GetRequiredService<IOrderService>();
    }

    [Fact]
    public async Task PlaceOrder_ShouldCreatePendingOrder()
    {
        // Act & Assert
        var result = await _orderService.PlaceOrder(new OrderRequest());
        Assert.True(result.IsSuccess);
    }
}

Why This Matters

When you define strict boundaries, your tests become significantly faster because you aren't bootstrapping the entire monolith. Furthermore, these tests act as a "consumer" of your module's public API. If you find it difficult to write an isolated test for a module, it is a clear architectural warning sign that your module boundaries are leaking or that you have circular dependencies. By forcing isolation, you naturally push your design toward cleaner, more decoupled code that adheres to the Ports and Adapters principle.

Key Takeaway: Define clear public interfaces for your modules and use dependency injection to swap out cross-module dependencies, allowing you to verify module behavior in complete isolation from the rest of the system.