4

I have a couple of environment variables that are used as part of creating defaults for config options. Basically if the config options property in question is null then use the value from the specified environment variable.

I want to test the options objects correctly use the environment variable when null, so initially thought I'd just set the specified environment variable. However, this is an environment variable that will actually be used on the environment and therefore I don't really want to be messing with it. Is there a way I can wrap environment variables using an interface or something so I can mock the SetEnironmentVariable() method or something similar?

In terms of code my options objects look something like this:

public sealed record MyOptions
{
    public string SomeProperty { get; set; }

    public MyOptions()
    {
        var defaultVal = Environment.GetEnvironmentVariable("MY_VARIABLE");
        SomeProperty = SomeProperty ?? defaultVal;
    }
}

So if the SomeProperty hasn't been set via config, it will use the default value from the specified environment variable. I was thinking perhaps an EnvironmentVariableService, that retrieves all environment variables and puts them in a ConcurrentDictionary, and that's used in the place of the Environment.

sr28
  • 4,728
  • 5
  • 36
  • 67
  • 1
    So you basically want to check if `GetEnvironmentVariable` works. And to do that you want to replace it with something else and test that. So, what you would really be testing is that thing you replaced it with. Stop looking at your code coverage and check if tests actually make sense. – Palle Due Jan 17 '23 at 13:24
  • 1
    Writing code to be unit-testable is often about implementing a layer of abstraction so things get exchangeable. So yes putting that stuff into a service and making that service exchangable is the way to go. – Ralf Jan 17 '23 at 13:27
  • @PalleDue Not quite. I wanted to test that the config options property value was getting set at the right time as well as whether it's setting or overwriting the correct value if supplied via config. I then want to test this in conjunction with validation. – sr28 Jan 17 '23 at 13:28

1 Answers1

1

Your question contains the answer.

It is quite normal to wrap common .NET concrete implementations such as a timer, DateTime.Now, and utility class such as Environment and then inject that wrapper into the places that would normally use the concrete implementation.

Your case is a little unique in that you need access inside a sealed record. I would recommend you create a singleton of the service in that case. Ideally you avoid this approach.

The wrapper acts as an abstraction as mentioned in the comments to your question, and allows for the implementation to be swapped out in circumstances such as unit tests.

I happen to have implemented such a thing as I had this need as well. Here's the code.

public interface IEnvironmentService
{
    string? GetEnvironmentVariable(string name);
    void SetEnvironmentVariable(string name, string? value);
}

public class MockEnvironmentService : IEnvironmentService
{
    public string? GetEnvironmentVariable(string name)
    {
        var found = !_dictionary.TryGetValue(name, out var value);
        if (!found) value = null;
        _actions.Add(new GetAction(name, value, found));
        return value;
    }

    public void SetEnvironmentVariable(string name, string? value)
    {
        _dictionary[name] = value;
        _actions.Add(new SetAction(name, value));
    }

    public IReadOnlyCollection<Action> AllActions => _actions.ToArray();

    public IReadOnlyCollection<GetAction> GetActions => _actions.OfType<GetAction>().ToArray();

    public IReadOnlyCollection<SetAction> SetActions => _actions.OfType<SetAction>().ToArray();

    // see note after the code
    private readonly Dictionary<string, string?> _dictionary = new(StringComparer.InvariantCultureIgnoreCase);
    private readonly List<Action> _actions = new();
}

public class EnvironmentVariableService : IEnvironmentService
{
    public string? GetEnvironmentVariable(string name) => Environment.GetEnvironmentVariable(name);

    public void SetEnvironmentVariable(string name, string? value) => Environment.SetEnvironmentVariable(name, value);
}

public record Action(string Name, string? Value);

public record SetAction(string Name, string? Value) : Action(Name, Value);

public record GetAction(string Name, string? Value, bool WasFound) : Action(Name, Value);

At the beginning of your tests, if you need to preset/seed some environment variables to test read-side code, you can call MockEnvironmentService.SetEnvironmentVariable().

The implementation above uses a case insensitive dictionary for the keys, but whether names are case sensitive really depends on the OS.

Finally, the mock records the get/set calls in the order they occur, and you can use the properties in your assertions.

Kit
  • 20,354
  • 4
  • 60
  • 103