Matthew MacFarland
Matthew MacFarland

Reputation: 2731

How can the FileInfo class be mocked using System.IO.Abstractions?

I have injected the System.IO.Abstractions.IFileSystem interface into a class so that I can unit test file system interactions. There is one place in the class that uses new FileInfo(fileName). What is the replacement for that when using the IFileSystem interface and MockFileSystem?

Replacing File.OpenRead with _fileSystem.File.OpenRead is simple...

public string? Decrypt(string encryptedFilePath, string privateKeyArmor, string passPhrase)
    {
        try
        {
            using var privateKeyStream = new MemoryStream(Encoding.ASCII.GetBytes(privateKeyArmor));
            using var encryptedFileStream = _fileSystem.File.OpenRead(encryptedFilePath);

            var inputStream = PgpUtilities.GetDecoderStream(encryptedFileStream);
...

...but I don't know how to replace new FileInfo(fileName) here.

private byte[] CompressFile(string fileName, CompressionAlgorithmTag algorithm)
    {
        var outputStream = new MemoryStream();
        var compressedDataGen = new PgpCompressedDataGenerator(algorithm);
        PgpUtilities.WriteFileToLiteralData(compressedDataGen.Open(outputStream), PgpLiteralData.Binary,
            new FileInfo(fileName));
...

I tried _fileSystem.FileInfo.FromFileName(fileName), but that returns IFileInfo instead of FileInfo and the WriteFileToLiteralData method won't take that.

Upvotes: 1

Views: 2107

Answers (1)

AlanT
AlanT

Reputation: 3663

There is a helper function FileInfo.New(string fileName) which can be used to create/use a mock IFileInfo object

public class FileInfoTest
{
    private readonly IFileSystem _fileSystem;

    public FileInfoTest()
        : this (new FileSystem())

    {
    }

    internal FileInfoTest(IFileSystem fileSystem)
    {
        _fileSystem = fileSystem;
    }

    public bool GetIsReadOnly(string path)
    {
        var info = _fileSystem.FileInfo.New(path);
        return info.IsReadOnly;
    }

}

To demonstrate this I have a physical file which is not read-only.

The first test, returns the IsReadonly state of the physical file.
The second, returns a mocked IFileInfo object with IsReadOnly set to true.

[TestMethod]
public void CheckFileInfoAgainstPhysicalFile()
{
    var tester = new FileInfoTest();
    var isReadOnly = tester.GetIsReadOnly(@"c:\dev\File.txt");
    Assert.IsFalse(isReadOnly);
}

[TestMethod]
public void CheckFileInfoAgainstMock()
{
    var mockFileInfo = new Mock<IFileInfo>();
    mockFileInfo.SetupGet(mk => mk.IsReadOnly).Returns(true);   
    var mockFileSystem = new Mock<IFileSystem>();
    mockFileSystem.Setup(mk => mk.FileInfo.New(@"c:\dev\File.txt")).Returns(mockFileInfo.Object);
        
    var tester = new FileInfoTest(mockFileSystem.Object);
    var isReadOnly = tester.GetIsReadOnly(@"c:\dev\File.txt");
    Assert.IsTrue(isReadOnly);

}

As mentioned in the comment, the above doesn't address the basic problem - PgpUtilities doesn't know what an IFileInfo is.

There is a way to fix this but it may not be worth the effort.

  1. Define an interface for the methods used from the static class.
  2. Inject this interface.
  3. In the default implementation of `IPgpUtilities`, use reflection to get at the `FileInfo` instance inside the `FileInfoWrapper` and pass it through to the external routine.
// Stand-in for External utility (returns a string so we can see it 
//  doing something with the original file)

public static class PgpUtilitiesOriginal
{
    public static string WriteFileToLiteralData(Stream outputStream,
                                    char fileType,
                                    FileInfo file)
    {
        return file.Name;
    }
}


// Interface for injection
public interface IPgpUtilties
{
    string WriteFileToLiteralData(Stream outputStream,
                                char fileType,
                                IFileInfo file);
}


// Wrapper for the External Utility
public class DefaultPgpUtilities : IPgpUtilties
{
    public string WriteFileToLiteralData(Stream outputStream, char fileType, IFileInfo file)
    {
        var instanceInfo = file.GetType().GetField("instance", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        var instance = (FileInfo)instanceInfo.GetValue(file);

        return PgpUtilitiesOriginal.WriteFileToLiteralData(outputStream, fileType, instance);
    }
}

// Test Target
public class Tester
{

    private readonly IFileSystem _fileSystem;
    private readonly IPgpUtilties _pgpUtilities;

    public Tester()
        : this(new FileSystem(), new DefaultPgpUtilities())
    {
    }

    public Tester(IFileSystem fileSystem, IPgpUtilties pgpUtilities)
    {
        _fileSystem = fileSystem;
        _pgpUtilities = pgpUtilities;
    }

    public string Run(string fileName)
    {
        return _pgpUtilities.WriteFileToLiteralData(null, '\0', _fileSystem.FileInfo.FromFileName(fileName));
    }
}


[TestMethod]
public void PhysicalFile()
{
    var tester = new Tester();
    var ret = tester.Run(@"c:\dev\file.txt");
    Assert.AreEqual("file.txt", ret);
}


[TestMethod]
public void MockedFile()
{
    var mockFileObject = new Mock<IFileInfo>();
        
        
    var mockFileSystem = new Mock<IFileSystem>();
    mockFileSystem.Setup(mk => mk.FileInfo.FromFileName(@"c:\dev\file.txt")).Returns(mockFileObject.Object);

    var mockPgpUtilties = new Mock<IPgpUtilties>();
    mockPgpUtilties.Setup(mk => mk.WriteFileToLiteralData(It.IsAny<Stream>(), It.IsAny<char>(), mockFileObject.Object)).Returns("Hello World");

    var tester = new Tester(mockFileSystem.Object, mockPgpUtilties.Object);


    var ret= tester.Run(@"c:\dev\file.txt");
    Assert.AreEqual("Hello World", ret);
}

Again, sorry about the piss-poor reading of the original question on my part.

Upvotes: 1

Related Questions