Published on

Simplify and Amplify: Mocking IConfiguration in .NET the Correct Way

9 min read
Authors
  • avatar
    Name
    Ivan Gechev
    Twitter

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:

ILoggingService.cs
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:

LoggingService.cs
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:

appsettings.json
{
  "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:

PackageManager
Install-Package Moq

Then, we can write our test method:

LoggingServiceTests.cs
[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:

PackageManager
Install-Package NSubstitute

Once the package is isntalled, we can start writing our test:

LoggingServiceTests.cs
[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:

LoggingServiceTests.cs
[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.