- Published on
Simplify and Amplify: Mocking IConfiguration in .NET the Correct Way
- Authors
- Name
- Ivan Gechev
In the world of .NET applications, configuration plays a vital role in determining the behavior and settings of our applications. However, when it comes to testing, configuration can present a significant challenge. That's where our mocking abilities can be truly tested. In this post, we'll dive in and unlock the secrets of simplifying and amplifying IConfiguration
mocking in .NET.
The Code
We'll use a very simple LoggingService
that uses IConfiguration
. We start by defining the interface:
public interface ILoggingService
{
bool LogToConsole(string message);
}
We define an interface called ILoggingService
and declare a single LogToConsole
method, which takes a message as input and returns a boolean value indicating the success or failure of logging the message to the console.
Next, we define our concrete implementation:
public class LoggingService : ILoggingService
{
private readonly IConfiguration _configuration;
public LoggingService(IConfiguration configuration)
{
_configuration = configuration;
}
public bool LogToConsole(string message)
{
bool loggingEnabled = _configuration.GetValue<bool>("Logging:Enabled");
int maxMessageLength = _configuration.GetValue<int>("MaxMessageLength");
if (!loggingEnabled ||
message.Length > maxMessageLength)
{
return false;
}
Console.WriteLine(message);
return true;
}
}
We create the LoggingService
class that implements the ILoggingService
interface. We also create a constructor that takes an IConfiguration
parameter, allowing the configuration to be injected into our class. In the LogToConsole
method we retrieve configuration values for whether logging is enabled as well as the maximum message length. Then we log the message to the console only if logging is enabled and the message length is within the allowed limit. If the message has been logged successfully we return true
, otherwise we return false
.
Finally, here is what our configuration file looks like:
{
"Log": {
"Enabled": true
},
"MaxMessageLength": 50
}
Our configuration file is very simple. We have a property Log
that has an Enabled
boolean value set to true
, and another property - MaxMessageLength
with a numeric value of 50.
We have everything ready for testing, let's see how we can do that.
Mocking IConfiguration Using a Framework
The popular approach for mocking IConfiguration
in .NET is utilizing a mocking framework. With the help of frameworks such as Moq
or NSubstitute
, we can streamline the process of mocking and focus on writing effective tests. We'll use xUnit
to showcase how we can mock IConfiguration
in .NET.
Mocking IConfiguration Using Moq
To start using Moq
, we need to install it:
Install-Package Moq
Then, we can write our test method:
[Fact]
public void WhenLogToConsoleIsInvoked_ThenTrueIsReturned_UsingMoq()
{
// Arrange
var mockedLogSection = new Mock<IConfigurationSection>();
mockedLogSection.Setup(x => x.Value).Returns("true");
var mockedLengthSection = new Mock<IConfigurationSection>();
mockedLengthSection.Setup(x => x.Value).Returns("50");
var configuration = new Mock<IConfiguration>();
configuration.Setup(x => x.GetSection("Log:Enabled"))
.Returns(mockedLogSection.Object);
configuration.Setup(x => x.GetSection("MaxMessageLength"))
.Returns(mockedLengthSection.Object);
var loggingService = new LoggingService(configuration.Object);
string message = "Test message";
// Act
bool result = loggingService.LogToConsole(message);
// Assert
result.Should().BeTrue();
}
We write a test to verify that when the LogToConsole
method of the LoggingService
class is invoked with a specific message, the expected behavior is for the method to return true
. To start, we mock two IConfigurationSection
objects using Moq
. They represent the Log:Enabled
and MaxMessageLength
sections. We configure these mocked sections to return the desired values.
Then, we set up a mock IConfiguration
and configure it to return specific values for the Log:Enabled
and MaxMessageLength
keys using the Setup
and Returns
methods on our Mock<IConfiguration>
variable. Note that we use the GetSection
method instead of the GetValue<T>
. This is because GetValue<T>
calls GetSection
behind the scenes and its result can't be mocked directly.
After this is done, we create an instance of LoggingService
using the mocked configuration, invoke the LogToConsole
method, and assert that the returned result is true
using the FluentAssertions
library.
Mocking IConfiguration Using NSubstitute
First, we install NSubstitute
:
Install-Package NSubstitute
Once the package is isntalled, we can start writing our test:
[Fact]
public void WhenLogToConsoleIsInvoked_ThenTrueIsReturned_UsingNSubstitute()
{
// Arrange
var mockedLogSection = Substitute.For<IConfigurationSection>();
mockedLogSection.Value.Returns("true");
var mockedLengthSection = Substitute.For<IConfigurationSection>();
mockedLengthSection.Value.Returns("50");
var configuration = Substitute.For<IConfiguration>();
configuration.GetSection("Log:Enabled")
.Returns(mockedLogSection);
configuration.GetSection("MaxMessageLength")
.Returns(mockedLengthSection);
var loggingService = new LoggingService(configuration);
string message = "Test message";
// Act
var result = loggingService.LogToConsole(message);
// Assert
result.Should().BeTrue();
}
As with the Moq
example, we first need to create mocked instances of IConfigurationSection
but this time using NSubstitute
. We configure the substitutes to return specific values for the Log:Enabled
and MaxMessageLength
keys.
Next, we create a substitute for IConfiguration
and set up what it has to return. We achieve this by invoking the Returns
method on GetSection
method of IConfiguration
. We use GetSection
and not GetValue<T>
for the reasons mentioned before.
Finally, we instantiate the LoggingService
with the substituted configuration, invoke the LogToConsole
method, and assert that the returned result is true
, ensuring that the logging was successful.
Build IConfiguration Instead of Mocking It
But mocking IConfigurations
only complicates things for us. Instead of using a mocking framework, we can create a custom implementation of IConfiguration
that provides the desired configuration values for our tests. This approach allows us to simulate different configuration scenarios without relying on external dependencies or frameworks.
Here's how we can achieve that:
[Fact]
public void WhenLogToConsoleIsInvoked_ThenTrueIsReturned_NoFramework()
{
// Arrange
var inMemorySettings = new Dictionary<string, string?> {
{"Log:Enabled", "true"},
{"MaxMessageLength", "50"}};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(inMemorySettings)
.Build();
var loggingService = new LoggingService(configuration);
string message = "Test message";
// Act
var result = loggingService.LogToConsole(message);
// Assert
result.Should().BeTrue();
}
We start by initializing an inMemorySettings
dictionary with key-value pairs representing configuration settings. Our first key is Log:Enabled
with a value of true
and our second key - "MaxMessageLength" has a value of 50
.
Then, we use ConfigurationBuilder
to add the in-memory settings. We do that by passing our dictionary to the AddInMemoryCollection
method and then building the final configuration object. Then we proceed with the already familiar, and identical, Act and Assert parts of our tests.
This approach of "mocking" IConfiguration
allows us to create and customize the configuration dynamically and in memory with the need for overly complex mocking techniques.
Conclusion
In conclusion, when it comes to testing code that relies on IConfiguration
, building the configuration instead of mocking it can provide a simpler and more flexible approach. By creating a custom implementation of IConfiguration
using in-memory settings, we can easily simulate different configuration scenarios without the need for complex mocking configurations. This approach also allows us to have full control over the configuration values and eliminates the dependency on external frameworks during testing. It simplifies the testing process and provides us with a more straightforward way to verify the behavior of code that depends on configuration settings.