10

I created an extension method to add all JSON configuration files to the IConfigurationBuilder

public static class IConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddJsonFilesFromDirectory(
        this IConfigurationBuilder configurationBuilder,
        IFileSystem fileSystem,
        string pathToDirectory,
        bool fileIsOptional,
        bool reloadConfigurationOnFileChange,
        string searchPattern = "*.json",
        SearchOption directorySearchOption = SearchOption.AllDirectories)
    {
        var jsonFilePaths = fileSystem.Directory.EnumerateFiles(pathToDirectory, searchPattern, directorySearchOption);

        foreach (var jsonFilePath in jsonFilePaths)
        {
            configurationBuilder.AddJsonFile(jsonFilePath, fileIsOptional, reloadConfigurationOnFileChange);
        }

        return configurationBuilder;
    }
}

and want to create tests for it using xUnit. Based on

How do you mock out the file system in C# for unit testing?

I installed the packages System.IO.Abstractions and System.IO.Abstractions.TestingHelpers and started to test that JSON files from directories have been added

public sealed class IConfigurationBuilderExtensionsTests
{
    private const string DirectoryRootPath = "./";
    
    private readonly MockFileSystem _fileSystem;

    public IConfigurationBuilderExtensionsTests()
    {
        _fileSystem = new MockFileSystem(new[]
            {
                "text.txt", 
                "config.json", 
                "dir/foo.json", 
                "dir/bar.xml", 
                "dir/sub/deeper/config.json"
            }
            .Select(filePath => Path.Combine(DirectoryRootPath, filePath))
            .ToDictionary(
            filePath => filePath, 
            _ => new MockFileData(string.Empty)));
    }
    
    [Theory]
    [InlineData("*.json", SearchOption.AllDirectories)]
    [InlineData("*.json", SearchOption.TopDirectoryOnly)]
    // ... more theories go here ...
    public void ItShouldAddJsonFilesFromDirectory(string searchPattern, SearchOption searchOption)
    {
        var addedJsonFilePaths = new ConfigurationBuilder()
            .AddJsonFilesFromDirectory(_fileSystem, DirectoryRootPath, true, true, searchPattern, searchOption)
            .Sources
            .OfType<JsonConfigurationSource>()
            .Select(jsonConfigurationSource => jsonConfigurationSource.Path)
            .ToArray();
        
        var jsonFilePathsFromTopDirectory = _fileSystem.Directory.GetFiles(DirectoryRootPath, searchPattern, searchOption);
        
        Assert.True(addedJsonFilePaths.Length == jsonFilePathsFromTopDirectory.Length);
    
        for (int i = 0; i < addedJsonFilePaths.Length; i++)
        {
            Assert.Equal(
                jsonFilePathsFromTopDirectory[i],
                Path.DirectorySeparatorChar + addedJsonFilePaths[i]);
        }
    }
}

The tests are passing but I would like to know if I could get in trouble when prepending Path.DirectorySeparatorChar to addedJsonFilePaths[i].

The problem is that

  • jsonFilePathsFromTopDirectory[i] returns "/config.json"
  • addedJsonFilePaths[i] returns "config.json"

so I have to prepend a slash at the beginning. Do you have any suggestions how to improve this / avoid later problems?

Question3r
  • 2,166
  • 19
  • 100
  • 200

3 Answers3

3

/config.json is an absolute path and config.json is a relative path, so to compare them you have to convert the relative path into absolute path by giving it a directory.

But this isn't the real problem, the document is not detailed enough (In fact it doesn't mention about this at all).

When you add a path by AddJsonFile extension method, it will automatically call FileConfigurationSource.ResolveFileProvider.

If no file provider has been set, for absolute Path, this will creates a physical file provider for the nearest existing directory.

This method convert the absolute path into a relative path, that's why /config.json becomes config.json, the directory info is put into an auto-generated file provider.

So to use the API correctly, you need change:

jsonConfigurationSource.Path

to:

jsonConfigurationSource.FileProvider.GetFileInfo(jsonConfigurationSource.Path).PhysicalPath

Or you can provide a FileProvider:

configurationBuilder.AddJsonFile(new NullFileProvider(), jsonFilePath, fileIsOptional, reloadConfigurationOnFileChange);

shingo
  • 18,436
  • 5
  • 23
  • 42
2

Instead of adding the directory separator character yourself, you might use System.IO.Path.Combine, which takes cares of that; it only adds one if needed.

Side note: since jsonFilePathsFromTopDirectory[i] returns /config.json having a / instead of a \, you might consider to use Path.AltDirectorySeparatorChar / instead of Path.DirectorySeparatorChar \.
In either way, Path.Combine deals with both.


Both below statements result in /config.json.

Path.Combine(Path.AltDirectorySeparatorChar.ToString(), "/config.json");
Path.Combine(Path.AltDirectorySeparatorChar.ToString(), "config.json");

Your assert statement would look like

Assert.Equal(
    jsonFilePathsFromTopDirectory[i],
    Path.Combine(Path.AltDirectorySeparatorChar.ToString(), addedJsonFilePaths[i])
    );
pfx
  • 20,323
  • 43
  • 37
  • 57
  • Hey, thanks for your answer! I personally prefer this answer because then I don't need the loop anymore, I can just check for array equality https://stackoverflow.com/a/70410993/7764329 – Question3r Dec 19 '21 at 15:50
  • @Question3r That's all fine, there's often more than 1 way to achieve the expected result. It's up to you to chose the one that fits best for your case. – pfx Dec 19 '21 at 15:55
1

The logic of comparing files seems alright, I don't find any outstanding problem with it, it is ok to prepend the "/" to match what you need. Could be even better if you could use the System.IO.Path.DirectorySeparatorChar for the directory root path as well, so if you run on windows or Linux you will have no issues.

But there may be a conceptual problem with what you are doing. To my understanding you aim to verify existence of specific configuration files required for your program to work right, if those files are missing than the program should fail. But that kind of failure due to missing configuration files, is an expected and valid result of your code. Yet, you unit-test this as if missing files should fail the test, as if missing files are an indication that something wrong with your code, this is wrong.

Missing files are not indication of your code not working correct and Unit-test should not be used as a validator to make sure the files exist prior executing the program, you will likely agree that unit-test is not part of the actual process and it should only aim to test your code and not preconditions, the test should compare an expected result (mock result of your code) vs. actual result and certainly not meant to become part of the code. That unit test looks like a validator that should be in the code.

So unless those files are produced by your specific code (and not the deployment) there is no sense testing that. In such case you need to create a configuration validator code - and your unit test could test that instead. So it will test that the validator expected result with a mock input you provide. But the thing here is that you would know that you only testing the validation logic and not the actual existence of the files.

G.Y
  • 6,042
  • 2
  • 37
  • 54