It can be difficult to write unit tests for code that accesses the file system.
It’s possible to write integration tests that read in an actual file from the file system, do some processing, and check the resultant output file (or result) for correctness. There are a number of potential problems with these types of integration tests including the potential for them to more run slowly (real IO access overheads), additional test file management/setup code, etc. (this does not mean that some integration tests wouldn’t be useful however).
The System.IO.Abstractions NuGet package can help to make file access code more testable. This package provides a layer of abstraction over the file system that is API-compatible with existing code.
Take the following code as an example:
using System.IO;
namespace ConsoleApp1
{
public class FileProcessorNotTestable
{
public void ConvertFirstLineToUpper(string inputFilePath)
{
string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");
using (StreamReader inputReader = File.OpenText(inputFilePath))
using (StreamWriter outputWriter = File.CreateText(outputFilePath))
{
bool isFirstLine = true;
while (!inputReader.EndOfStream)
{
string line = inputReader.ReadLine();
if (isFirstLine)
{
line = line.ToUpperInvariant();
isFirstLine = false;
}
outputWriter.WriteLine(line);
}
}
}
}
}
The preceding code opens a text file, and writes it to a new output file, but with the first line converted to uppercase.
This class is not easy to unit test however, it is tightly coupled to the physical file system with the calls to File.OpenText and File.CreateText.
Once the System.IO.Abstractions NuGet package is installed, the class can be refactored as follows:
using System.IO;
using System.IO.Abstractions;
namespace ConsoleApp1
{
public class FileProcessorTestable
{
private readonly IFileSystem _fileSystem;
public FileProcessorTestable() : this (new FileSystem()) {}
public FileProcessorTestable(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public void ConvertFirstLineToUpper(string inputFilePath)
{
string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");
using (StreamReader inputReader = _fileSystem.File.OpenText(inputFilePath))
using (StreamWriter outputWriter = _fileSystem.File.CreateText(outputFilePath))
{
bool isFirstLine = true;
while (!inputReader.EndOfStream)
{
string line = inputReader.ReadLine();
if (isFirstLine)
{
line = line.ToUpperInvariant();
isFirstLine = false;
}
outputWriter.WriteLine(line);
}
}
}
}
}
The key things to notice in the preceding code is the ability to pass in an IFileSystem as a constructor parameter. The calls to File.OpenText and File.CreateText are now redirected to _fileSystem.File.OpenText and _fileSystem.File.CreateText respectively.
If the parameterless constructor is used (e.g. in production at runtime) an instance of FileSystem will be used, however at test time, a mock IFileSystem can be supplied.
Handily, the System.IO.Abstractions.TestingHelpers NuGet package provides a pre-built mock file system that can be used in unit tests, as the following simple test demonstrates:
using System.IO.Abstractions.TestingHelpers;
using Xunit;
namespace XUnitTestProject1
{
public class FileProcessorTestableShould
{
[Fact]
public void ConvertFirstLine()
{
var mockFileSystem = new MockFileSystem();
var mockInputFile = new MockFileData("line1\nline2\nline3");
mockFileSystem.AddFile(@"C:\temp\in.txt", mockInputFile);
var sut = new FileProcessorTestable(mockFileSystem);
sut.ConvertFirstLineToUpper(@"C:\temp\in.txt");
MockFileData mockOutputFile = mockFileSystem.GetFile(@"C:\temp\in.out.txt");
string[] outputLines = mockOutputFile.TextContents.SplitLines();
Assert.Equal("LINE1", outputLines[0]);
Assert.Equal("line2", outputLines[1]);
Assert.Equal("line3", outputLines[2]);
}
}
}
To see this in action or to learn more about file access, check out my Working with Files and Streams in C# Pluralsight course.
You can start watching with a Pluralsight free trial.
SHARE: