Modular monoliths in .NET let you structure your application as distinct modules like Product, Order, and User without breaking it into separate services. Testing in this setup isn’t just helpful, it’s necessary. Unit tests are important to check how each module behaves in isolation, while integration tests are important to see if they work together as intended in real scenarios.
Types of Tests in Modular Monolith
Test Type | What it Tests | Scope |
Unit Test | Individual classes/methods | Internal logic |
Integration Test | Interaction between modules or with real database | System workflows |
End-to-End Test (optional) | Full app (UI/API to DB) | External behavior |
Example App Folder Structure
MyApp/
├── Modules/
│ ├── Product/
│ │ ├── ProductService.cs
│ │ └── ProductRepository.cs
│ ├── Order/
│ │ └── OrderService.cs
├── API/
│ └── Controllers/
├── Tests/
│ ├── UnitTests/
│ └── IntegrationTests/
How to Conduct Unit Testing in Modular Monolith .NET (Step-by-Step)
Step 1: Add xUnit and Moq to your test project
dotnet add package xunit
dotnet add package Moq
Step 2: Create Unit Test for Product Module
ProductService.cs
public class ProductService
{ private readonly IProductRepository _repo; public ProductService(IProductRepository repo) { _repo = repo; } public Product GetById(int id) => _repo.GetById(id);
}
ProductServiceTests.cs
public class ProductServiceTests
{ [Fact] public void GetById_ShouldReturnProduct() { // Arrange var mockRepo = new Mock<IProductRepository>(); mockRepo.Setup(x => x.GetById(1)).Returns(new Product { Id = 1, Name = "Test" }); var service = new ProductService(mockRepo.Object); // Act var result = service.GetById(1); // Assert Assert.NotNull(result); Assert.Equal("Test", result.Name); }
}
Integration Testing in Modular Monolith (Step-by-Step)
Step 1: Add Required NuGet Packages
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package FluentAssertions
Step 2: Create In-Memory Test Server
ProductIntegrationTests.cs
public class ProductIntegrationTests : IClassFixture<WebApplicationFactory<Program>>{ private readonly HttpClient _client; public ProductIntegrationTests(WebApplicationFactory<Program> factory) { _client = factory.CreateClient(); // In-memory test server } [Fact] public async Task GetProductById_ReturnsProduct() { // Act var response = await _client.GetAsync("/api/products/1"); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); var product = JsonConvert.DeserializeObject<Product>(json); // Assert product.Should().NotBeNull(); product.Id.Should().Be(1); }
}
Purpose: This tests the real behavior of the app, with routing, controllers, and DB (in-memory).
Step 3: Configure In-Memory DB in Program.cs for testing
if (app.Environment.IsEnvironment("Testing"))
{ builder.Services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("TestDb"));
}
Or override it using WebApplicationFactory<T> in your test setup.
Best Practices for all Testing in Modular Monolith
Aspect | Recommendation |
Unit Tests | Fast, mock dependencies, and cover edge cases |
Integration Tests | Use TestServer, real or in-memory databases |
Foldering | Organize tests by module for clarity |
Test Boundaries | Verify communication between modules is correct |
Isolation | Clean the database before each integration test |
Performance | Keep integration tests fewer, but focused and meaningful |
Naming | Follow naming like ProductServiceTests.cs or OrderIntegrationTests.cs |
Final Thought
Unit tests help confirm that each module works as intended, while integration tests ensure different modules interact properly in real-world scenarios. If you’re building a modular monolith and want reliable, scalable testing strategies, it’s a smart move to hire .NET developers who understand how to balance isolation with real-world validation. That’s how you keep your application modular, maintainable, and production-ready.