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.