If you've ever found yourself knee-deep in mock setup code just to test a single class, you're not alone.
Unit testing in .NET often starts simple, until your system under test depends on multiple dependencies, and you're suddenly mocking multiple layers of indirection just to verify a single method call. That’s where DepenMock comes in.
This post introduces DepenMock: a lightweight, zero-configuration C# library that lets you create a system under test with a fully mocked dependency tree in one line of code.
Why I Built DepenMock
I wanted something that:
- Automatically builds the full constructor chain
- Gives me access to any mock at any level
- Uses Moq under the hood (because it's familiar and powerful)
- Lets me focus on what I’m testing, not how I’m wiring up dependencies
- And yes, lets me test logging messages
While working in large legacy codebases, often described as "big balls of mud", I constantly ran into challenges writing meaningful tests. The service and business classes I encountered were bloated with dependencies, clearly violating the Single Responsibility Principle. This made it frustratingly difficult to isolate a single class into a test harness. In many cases, the test setup was more complex than the actual test logic.
I wanted a better way to test a class based solely on how a client would use it — through its interface. If a method wasn’t part of the interface, it shouldn’t be public or testable, because it wasn’t part of the contract. By focusing tests on interface-defined behavior, not internal implementation, I could better uphold principles like the Liskov Substitution Principle and keep tests focused, maintainable, and aligned with actual usage.
Why I Test Logging
Logging is often treated as something you don't test, but in practice, I’ve found it to be critical—especially in systems where logs are your only visibility into production behavior. This is particularly true in event sourcing architectures, where logs represent a form of system state. When debugging production issues or validating business workflows, the presence or absence of specific log information can be the difference between finding a root cause or being left in the dark. I wanted DepenMock to support verifying that important messages were logged, so developers can treat logging as a first-class part of behavior, not just an implementation detail.
So I built DepenMock.
What Does DepenMock Do?
DepenMock recursively resolves constructor dependencies using mocks and gives you:
- A fully constructed instance of your system under test
- A dictionary of mocks you can configure or verify
- A way to generate test data
- A consistent and expressive API
Let’s look at an example.
Basic Example
Say you’re testing a class like this:
public class OrderProcessingService : IOrderProcessingService
{
private readonly IOrderRepository _orderRepository;
private readonly ICustomerService _customerService;
private readonly IPaymentGateway _paymentGateway;
private readonly ILogger<OrderProcessingService> _logger;
private readonly IEmailService _emailService;
private readonly IInventoryService _inventoryService;
private readonly IConfiguration _configuration;
private readonly IMetricsCollector _metricsCollector;
private readonly IFeatureToggleService _featureToggleService;
public OrderProcessingService(
IOrderRepository orderRepository,
ICustomerService customerService,
IPaymentGateway paymentGateway,
ILogger<OrderProcessingService> logger,
IEmailService emailService,
IInventoryService inventoryService,
IConfiguration configuration,
IMetricsCollector metricsCollector,
IFeatureToggleService featureToggleService)
{
_orderRepository = orderRepository;
_customerService = customerService;
_paymentGateway = paymentGateway;
_logger = logger;
_emailService = emailService;
_inventoryService = inventoryService;
_configuration = configuration;
_metricsCollector = metricsCollector;
_featureToggleService = featureToggleService;
}
public async Task<bool> ProcessOrderAsync(Guid orderId, string correlationId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
var order = await _orderRepository.GetOrderAsync(orderId);
if (order == null)
{
_logger.LogWarning("Order {OrderId} not found. Correlation Id: {CorrelationId}", orderId, correlationId);
return false;
}
var customer = await _customerService.GetCustomerAsync(order.CustomerId);
if (customer == null)
{
_logger.LogWarning("Customer {CustomerId} not found. Correlation Id: {CorrelationId}", order.CustomerId, correlationId);
return false;
}
var isInventoryAvailable = await _inventoryService.ReserveItemsAsync(order.Items);
if (!isInventoryAvailable)
{
_logger.LogWarning("Insufficient inventory for order {OrderId}. Correlation Id: {CorrelationId}", orderId, correlationId);
return false;
}
var paymentSuccess = await _paymentGateway.ChargeAsync(customer, order.TotalAmount);
if (!paymentSuccess)
{
_logger.LogError("Payment failed for order {OrderId}. Correlation Id: {CorrelationId}", orderId, correlationId);
return false;
}
await _emailService.SendOrderConfirmationAsync(customer.Email, order);
await _metricsCollector.RecordOrderProcessedAsync(orderId);
return true;
}
}
Normally, you’d have to set up all mocks manually to get this class in a test harness.
public class OrderProcessingManualTests
{
[Fact]
public async Task ProcessOrderAsync_SuccessfulFlow_LogsAndReturnsTrue()
{
// Arrange
var orderId = Guid.NewGuid();
var customerId = Guid.NewGuid();
var order = new Order
{
Id = orderId,
CustomerId = customerId,
Items = new[] { new OrderItem { ProductId = "P1", Quantity = 2 } },
TotalAmount = 100m
};
var customer = new Customer
{
Id = customerId,
Email = "test@example.com"
};
var correlationId = Guid.NewGuid().ToString();
var orderRepo = new Mock<IOrderRepository>();
var customerService = new Mock<ICustomerService>();
var paymentGateway = new Mock<IPaymentGateway>();
var logger = new Mock<ILogger<OrderProcessingService>>();
var emailService = new Mock<IEmailService>();
var inventoryService = new Mock<IInventoryService>();
var configuration = new Mock<IConfiguration>();
var metricsCollector = new Mock<IMetricsCollector>();
var featureToggleService = new Mock<IFeatureToggleService>();
orderRepo.Setup(r => r.GetOrderAsync(orderId)).ReturnsAsync(order);
customerService.Setup(c => c.GetCustomerAsync(customerId)).ReturnsAsync(customer);
inventoryService.Setup(i => i.ReserveItemsAsync(order.Items)).ReturnsAsync(true);
paymentGateway.Setup(p => p.ChargeAsync(customer, order.TotalAmount)).ReturnsAsync(true);
var sut = new OrderProcessingService(
orderRepo.Object,
customerService.Object,
paymentGateway.Object,
logger.Object,
emailService.Object,
inventoryService.Object,
configuration.Object,
metricsCollector.Object,
featureToggleService.Object);
// Act
var result = await sut.ProcessOrderAsync(orderId, correlationId);
// Assert
Assert.True(result);
emailService.Verify(e => e.SendOrderConfirmationAsync(customer.Email, order), Times.Once);
metricsCollector.Verify(m => m.RecordOrderProcessedAsync(orderId), Times.Once);
// Manually verify logging (basic style – Moq doesn't make this easy)
logger.Verify(l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains($"Processing order {orderId}")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once);
}
}
With DepenMock:
public class OrderProcessingTests : BaseTestByAbstraction<OrderProcessingService, IOrderProcessingService>
{
[Fact]
public async Task ProcessOrderAsync_SuccessfulFlow_LogsAndReturnsTrue()
{
// Arrange
var correlationId = Container.Create<string>();
var mockEmailService = Container.ResolveMock<IEmailService>();
var mockMetricsCollector = Container.ResolveMock<IMetricsCollector>();
var orderId = Container.Create<Guid>();
var customer = Container.Create<Customer>();
var order = Container
.Build<Order>()
.With(x => x.Customer, customer)
.Create();
Container.ResolveMock<IOrderRepository>()
.Setup(r => r.GetOrderAsync(orderId))
.ReturnsAsync(order);
Container.ResolveMock<ICustomerService>()
.Setup(c => c.GetCustomerAsync(order.CustomerId))
.ReturnsAsync(customer);
var sut = ResolveSut();
// Act
var result = await sut.ProcessOrderAsync(orderId, correlationId);
// Assert
result.ShouldBeTrue();
mockEmailService.Verify(e => e.SendOrderConfirmationAsync(customer.Email, order), Times.Once);
mockMetricsCollector.Verify(m =>m.RecordOrderProcessedAsync(orderId), Times.Once);
Logger.InformationLogs().ContainsMessage($"Processing order {orderId}");
}
}
In real tests, you’d typically have setup methods and one assertion per test, but this example demonstrates how DepenMock reduces code and produces (in my opinion) cleaner tests.
🧾 Summary Comparison
Feature | Manual Moq | DepenMock |
---|---|---|
Dependency Setup | Manual, repetitive | Auto-resolved with Container.ResolveMock<T>() |
Test Data Creation | Manually constructed | Auto-generated via .Create<T>() and .Build<T>() |
Logging Verification | Verbose and awkward | Clean and expressive with Logger.InformationLogs() or just Logger.Logs |
Interface-First Testing | Optional | Enforced via base test class |
Boilerplate | High | Low |
Readability | Reduced by mock setup clutter | Focused on test intent |
Refactoring Resilience | Fragile if constructor changes | Constructor changes are handled automatically |
Installing DepenMock
You can install it from NuGet:
dotnet add package DepenMock
Or via the Package Manager:
Install-Package DepenMock
There are test framework specific packages available for:
What’s Next?
This was a quick intro to what DepenMock is and why I created it. In the next post, I’ll show some advanced usages of DepenMock.
But for now, give it a try in one of your test projects—and enjoy writing fewer lines of test setup code.
Happy testing!