How to Take Full Control of Your Mocks with DepenMock
In the previous post, I introduced DepenMock, a simple way to mock entire dependency graphs without the manual wiring.
Now let's go deeper.
In this post, I'll walk through:
- Accessing and configuring any mock in the graph
- Replacing mocks with custom instances
- Testing multiple classes with same interface (i.e. Strategy Pattern)
Accessing Any Mock
When you use ResolveSut()
, it builds a mock for every interface in the constructor chain and lets you retrieve any of them with .ResolveMock<T>()
.
Let's say you have a class that processes orders:
public class OrderService
{
private readonly IPaymentProcessor _payment;
private readonly IEmailSender _email;
public OrderService(IPaymentProcessor payment, IEmailSender email)
{
_payment = payment;
_email = email;
}
public bool SubmitOrder(Order order)
{
var success = _payment.Charge(order.Total);
if (success) _email.Send(order.Email, "Thanks for your order!");
return success;
}
}
You set up a test to verify that a payment was charged.
// Assemble
var chargeAmount = 200.0;
var mockPaymentProcessor = Container.ResolveMock<IPaymentProcessor>();
var order = Container
.Build<Order>()
.With(x => x.Total, chargeAmount)
.Create();
var sut = ResolveSut();
// Act
sut.SubmitOrder(order);
// Assert
mockPaymentProcessor.Verify(x => x.Charge(chargeAmount), Times.Once);
The above test is testing the interactions between the sut and it's dependency, IPaymentProcessor, to test that a call to _payment.Charge()
was made with the amount of 200.0. The call, Container.ResolveMock<IPaymentProcessor>()
, registers and returns a mocked IPaymentProcessor object. This instance will be injected into the sut when you call ResolveSut()
.
Replacing Mocks with Custom Instances
If you want to do stateful testing, you can override individual dependencies. Let's say you want to do some partial, or full, integration testing. You have that ability with Container.Register<TInterfaceType, TInstanceType>(TInstanceType instance)
.
public class CustomPaymentProcessor : IPaymentProcessor
{
public bool Charge(double total)
{
AmountCharged = total;
return true;
}
public double AmountCharged { get; private set; }
}
Now, in your test, you can replace a mock instance of your IPaymentProcessor with a custom instance.
// Assemble
var chargeAmount = 200.0;
var customPaymentProcessor = new CustomPaymentProcessor();
Container.Register<IPaymentProcessor, CustomPaymentProcessor>(customPaymentProcessor);
var order = Container
.Build<Order>()
.With(x => x.Total, chargeAmount)
.Create();
var sut = ResolveSut();
// Act
sut.SubmitOrder(order);
// Assert
customPaymentProcessor.AmountCharged.Should().Be(chargeAmount);
The instance of CustomPaymentProcessor
is now injected into the sut instead of a mocked instance. This example uses stateful testing to test the amount charged.
Testing Multiple classes with Same Interface
Testing multiple classes that use the same interface, like the Strategy Pattern, usually involves a lot of copy/paste code, since the tests are usually similar. DepenMock helps eliminate that copy/paste code, while still allowing you to test using the implemented interface.
public interface IDataStreamer
{
Task<string> ReadDataAsync();
Task SaveAsync(string content);
}
public class MemoryStreamStrategy : IDataStreamer
{
private byte[] _data = [];
public async Task<string> ReadDataAsync()
{
using var memoryStream = new MemoryStream(_data);
using var reader = new StreamReader(memoryStream);
return await reader.ReadToEndAsync();
}
public async Task SaveAsync(string content)
{
if (string.IsNullOrWhiteSpace(content))
{
throw new ArgumentNullException(nameof(content));
}
_data = Encoding.UTF8.GetBytes(content);
return Task.CompletedTask;
}
}
public class FileStreamStrategy : IDataStreamer
{
private readonly string _filePath = Path.GetTempFileName();
public async Task<string> ReadDataAsync()
{
using var fileStream = File.OpenRead(_filePath);
using var reader = new StreamReader(fileStream);
return await reader.ReadToEndAsync();
}
public async Task SaveAsync(string content)
{
if (string.IsNullOrWhiteSpace(content))
{
throw new ArgumentNullException(nameof(content));
}
await File.WriteAllTextAsync(_filePath, content);
}
}
You could set up a base class for testing all IDataStreamer
instances.
public abstract class DataStreamerTests<TConcreteTestType> : BaseTestByAbstraction<TConcreteTestType, IDataStreamer> where TConcreteTestType : class, IDataStreamer
{
[Theory]
[InlineData("")]
[InlineData(null)]
public async Task WhenContentIsNullOrWhiteSpace_ThrowsException(string content)
{
// Arrange
var sut = ResolveSut();
// Act
Task SaveAsync() => sut.SaveAsync(content);
// Assert
await Assert.ThrowsAsync<ArgumentNullException>(SaveAsync);
}
[Fact]
public async Task WhenContentIsValid_SavesAndReadsCorrectly()
{
// Arrange
var sut = ResolveSut();
var content = "Hello, World!";
// Act
await sut.SaveAsync(content);
var result = await sut.ReadDataAsync();
// Assert
Assert.Equal(content, result);
}
}
Now, to test both MemoryStreamStrategy, as well as FileStreamStrategy, you just need to add individual test structures for both.
public class MemoryStreamTests : DataStreamerTests<MemoryStreamStrategy>
{
// Additional tests specific to MemoryStreamStrategy can be added here
}
public class FileStreamTests : DataStreamerTests<FileStreamStrategy>
{
// Additional tests specific to FileStreamStrategy can be added here
}
Both of these tests automatically inherit the tests from the base class, while still testing against the interface IDataStreamer
. Yet you still have the freedom to write instance specific tests if needed.
Summary
DepenMock goes far beyond simple dependency injection by giving you complete control over your test's dependency graph. Here's what we covered:
🔧 Access Any Mock: Use Container.ResolveMock<T>()
to access and configure any mock in your dependency chain, allowing you to verify interactions and set up complex scenarios without manual wiring.
🔄 Replace with Real Instances: Override individual dependencies with Container.Register<TInterface, TInstance>()
for partial integration testing or when you need stateful behavior that mocks can't provide.
📋 Test Strategy Patterns: Create base test classes that work with interfaces, then inherit from them for concrete implementations. This eliminates copy/paste code while maintaining full test coverage across different implementations of the same interface.
These advanced features make DepenMock a powerful tool for complex testing scenarios, whether you're testing deep dependency graphs, need partial integration testing, or want to efficiently test multiple implementations of the same interface. The framework handles the complexity while giving you the flexibility to test exactly what you need.
In the next post, I'll show how you can use DepenMock to become more productive with writing your unit tests.
Happy mocking!