Testing and Configuration in .NET Core

- 5 minutes read - 874 words

When running automated tests, or running things locally, I often want to use a different configuration to what I would run in production. A JSON file often suffices for local development, however this isn’t useful for automated tests where I want different configurations for different tests, or if my configuration is dynamic (e.g. I need to spin up a docker container during startup, and I have to get some configuration from that on the fly).

Luckily, the when using Microsoft.Extensions.Configuration, we have plenty of sources to load configuration from, including an InMemoryCollection source, which, as named, allows you to configure your configuration using an in memory collection.

For the samples used in this post, let’s define what we want our configuration to look like:

class MyAppConfig
{
    // Will use different values for tests
    public AuthConfig Auth { get; set; }

    public string[] EnabledTenants { get; set; }

    // In tests, this is maybe coming from a localstack instance running in docker
    // In production, will be coming from AWS directly
    public string QueueUrl { get; set; }

    public int DefaultTimeoutInSeconds { get; set; }
}

class AuthConfig
{
    public string Authority { get; set; }

    public string Audience { get; set; }
}

If we wanted to do the majority of our configuration in JSON, we may have a file that looks similar to this:

{
  "Auth": {
    "Authority": "https://example.com",
    "Audience": "https://example.com/myaudience"
  },
  "QueueUrl": "queue://some-made-up-thing",
  "EnabledTenants": ["Tenant1", "Tenant2"],
  "DefaultTimeoutInSeconds": 5
}

Now we have our basic configuration defined, let’s see how we can populate this in our unit tests.

Using InMemoryCollection in unit tests

As I mentioned before, I may want to have different configuration setups for different tests/suite of tests, which means using a JSON file isn’t practical.

Let’s look at an example that uses the InMemoryCollection

class MyExampleTests
{
    private readonly IConfiguration config;

    public MyExampleTests()
    {
        var configurationBuilder = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string>
            {
                ["QueueUrl"] = "http://localhost:4576/thisisfromsomesetup",
                ["Auth:Authority"] = "http://localhost:1234",
                ["Auth:Audience"] = "http://localhost:1234/myaud",
                ["EnabledTenants:0"] = "FirstTenant",
                ["EnabledTenants:1"] = "SecondTenant",
                ["DefaultTimeoutInSeconds"] = "5"
            });

        this.config = configurationBuilder.Build();
    }
}   

This is fine, and we’re up and running with our customised configuration in this test, however there are a few things I dislike about this approach.

Firstly, everything is stringly typed, meaning it’s really easy to have a typo in your keys, and you end up with some configuration that isn’t being bound properly. The values are also strings, notice how the DefaultTimeoutInSeconds is a string here, but we define it as an integer in the concrete type.

To mitigate some risk of typos, we could use string interpolation and the nameof operator for our keys:

class MyExampleTests
{
    private readonly IConfiguration config;

    public MyExampleTests()
    {
        var configurationBuilder = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string>
            {
                [$"{nameof(MyAppConfig.QueueUrl)}"] = "http://localhost:4576/thisisfromsomesetup",
                [$"{nameof(MyAppConfig.Auth)}:{nameof(MyAppConfig.Auth.Authority)}"] = "http://localhost:1234",
                [$"{nameof(MyAppConfig.Auth)}:{nameof(MyAppConfig.Auth.Audience)}"] = "http://localhost:1234/myaud",
                [$"{nameof(MyAppConfig.EnabledTenants)}:0"] = "FirstTenant",
                [$"{nameof(MyAppConfig.EnabledTenants)}:1"] = "SecondTenant",
                [$"{nameof(MyAppConfig.DefaultTimeoutInSeconds)}"] = "5"
            });

        this.config = configurationBuilder.Build();
    }
}

This makes it a little safer, but it’s quite difficult to read, especially on complex configurations, with more nested types.

I’m also not a fan of how arrays work here - because everything is strings, we’re forced to workaround this by using special keys to define the array.

If you ever switch between configuring your applications with environment variables and anything else, and if you are anything like me, you will confuse the separators for nested types too. Here, the separator is a colon, in environment variable, a double underscore. I always use __ instead of : when dealing with the InMemoryCollection, which leads to some confusion before I remember I need to use a colon instead.

Using the InMemoryCollection solves the problem we have, but I found it too prone to human error, and I wanted to find a better way.

Using types for in memory configuration

In case it wasn’t completely obvious, my biggest issue here is how everything is a string. I’m using a typed language, I want to use types! In fact, I already have a type defined for when I bind my IConfiguration to something a little more useful in my application, I want to achieve the same in my tests too.

To accomplish this, we need to write a little helper function (which I have defined as an extension method):

public static class ConfigurationHelpers
{
    public static IConfiguration ToConfiguration<T>(this T concreteType)
    {
        var jsonVersion = JsonConvert.SerializeObject(concreteType);

        return new ConfigurationBuilder()
            .AddJsonStream(
                new MemoryStream(Encoding.UTF8.GetBytes(jsonVersion)))
            .Build();
    }
}

All I’m doing here is taking a type, serialising it to JSON, and then using that JSON to build an in-memory stream, and using the same ConfigurationBuilder to turn that into our IConfiguration type that we pass into our system for testing purposes. It seems a little strange to do it this way, but we’re leaning on the ConfigurationBuilder as much as possible without writing our own provider.

Now, how does our example test setup look with use of this extension method:

class MyExampleTests
{
    private readonly IConfiguration config;

    public MyExampleTests()
    {
        var myConfig = new MyAppConfig
        {
            QueueUrl = "http://localhost:4576/thisisfromsomesetup",
            Auth = new AuthConfig
            {
                Authority = "http://localhost:1234",
                Audience = "http://localhost:1234/myaud"
            },
            EnabledTenants = new[] { "FirstTenant", "SecondTenant" },
            DefaultTimeoutInSeconds = 5
        };

        this.config = myConfig.ToConfiguration();
    }
}

Much better. I have type safety, it’s easier to read, and refactor safe.