Introduction to System.IO.Abstractions
One of the most
important things about writing testable code is the elimination or reduction of
dependencies between objects or components. Why is this important? Simply put, when
one object depends on another (which in turn may rely on another, and so on) you
can't truly say, "This is exactly how this works, I can guarantee
consistent results, and I can do it quickly."
Take, for example,
testing a piece of code that interacts with a database. An all-too-common
approach is to create a SQL query that sets the database to some known state,
and then run that query prior to your test. That may work but, if you're
working against a database with which others may be interacting, there's always
the chance that between running your SQL query and executing your test that the
state of the database has changed.
Going a step further,
there's the question of manageability. Are you really willing to write and
maintain dozens of these setup queries as your project grows? And, let's not
forget -- each of those queries is in itself a piece of code and can be
affected by others' test or development queries.
Plus, can the setup query
be integrated into an automated process, thus opening the door to regular
builds and continuous integration? Maybe. Maybe not.
And, finally, let's not
forget that any interaction with a database is slow, relative to the other
stuff your code is doing. Unit tests should be quick. There are at least two database transactions happening here. First, running a query to
set up your test. Second, the query within the code you originally wanted to test!
Every issue above also
affects file system operations. Instead of a database query that puts the appropriate
rows in place you now have a batch file that positions files in the correct
place. You get the picture. Indeed, quite a bit has been written about the evils of
testing dependency-laden code.
So, yes, reducing or
eliminating dependencies is important to writing testable code.
How do we accomplish
this when interacting with a file system? You could mock System.IO with
something like:
interface IFileSystem {
bool FileExists(string fileName);
DateTime GetCreationDate(string fileName);
}
But that's a lot of code
you're going to have to write (and test!).
Introducing System.IO.Abstractions
Someone has already done all the work and they have done it well. Head over to Github and take a look at System.IO.Abstractions.
Tatham Oddie
et al. have done the heavy lifting by
creating an interface (IFileSystem)
and a class (FileSystem)
plus a bevy of test
helpers which cover many of the scenarios you’re likely to encounter. Their
README says it best:
At the core of the
library is IFileSystem and FileSystem. Instead of calling methods like
File.ReadAllText directly, use IFileSystem.File.ReadAllText. We have exactly
the same API, except that ours is injectable and testable.
The README also provides
a few examples, which proved helpful in getting my own use of System.IO.Abstractions
off the ground.
An Example
In my case, I was
reading and writing a Microsoft Excel spreadsheet (an interesting exercise in OpenXML
and ClosedXML in itself) and
wanted to unit test the method I was using to read a path and file name from a
configuration file, find the file, open it, and then save it.
(My first step was to describe my method and note the use of the
word “and.” Anytime “and” makes an appearance in your description of a method,
it’s a sign that you’re asking the method to do too much. If you’re asking a
method to do more than one thing, how can you test it? A good test should only
cover one thing.)
Once I refactored my
code so, among other new methods, I had a method that did nothing more than
take in a string (path) and return a Stream. With that done, it was fairly easy
to design a test that described the expected result, provided a string (again, “path”),
grabbed the actual result, and compared the expected stream to the actual
stream.
What I ended up with was
the class I wanted to test:
/// <summary>
/// I really should document this stuff better
/// </summary>
public class MeterDataRepository
{
private static readonly ILog _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private readonly IFileSystem _fileSystem;
/// <summary>
/// The default constructor
/// </summary>
public MeterDataRepository()
{
_fileSystem = new FileSystem();
}
/// <summary>
/// A constructor that will accept an System.IO.Abstractions interface.
/// </summary>
/// <param name="fileSystem"></param>
public MeterDataRepository(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
/// <summary>
/// A constructor that opens one or more files AND extracts XML document(s)
/// </summary>
/// <returns></returns>
public List<MeterData> GetMeterDataFromFiles(string folderName, List<string> fileNames)
{
List<MeterData> results = new List<MeterData>();
if (fileNames == null) return results;
foreach (string fileName in fileNames)
{
Stream fileStream = _fileSystem.File.OpenRead(Path.Combine(folderName, fileName));
XmlDocument doc = ConvertStreamToXmlDoc(fileStream);
MeterData meter = ConvertXmlToMeter(doc);
results.Add(meter);
}
return results;
}
/// <summary>
/// Takes a Stream and returns an XmlDocument
/// </summary>
/// <param name="stream"></param>
/// <exception cref="ArgumentNullException"></exception>
/// <returns></returns>
internal XmlDocument ConvertStreamToXmlDoc(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("stream");
XmlDocument doc = new XmlDocument();
doc.Load(stream);
return doc;
}
/// <summary>
/// Takes a Stream and returns [severely abridged] gas meter data
/// </summary>
/// <param name="doc"></param>
/// <returns></returns>
public MeterData ConvertXmlToMeter(XmlDocument doc)
{
XmlNode serialNumberNode = doc.SelectSingleNode("/XML/DATA/SRL_NUM");
MeterData meter = new MeterData();
if (serialNumberNode != null)
{
meter.MeterNumber = serialNumberNode.InnerText;
}
XmlNode usageNode = doc.SelectSingleNode("/XML/DATA/SB/C0");
if (usageNode != null)
{
meter.Usage = Int32.Parse(usageNode.InnerText);
}
return meter;
}
}
And
my test code:
[TestMethod]
public void GetMeterDataFromFilesTest()
{
// arrange
const string folder = "C:";
List<string> fileNames = new List<string> { "meter01.xml", "meter02.xml", "meter03.xml" };
MeterData meter01 = new MeterData { MeterNumber = "1234", Usage = 100 };
MeterData meter02 = new MeterData { MeterNumber = "5678", Usage = 200 };
MeterData meter03 = new MeterData { MeterNumber = "9012", Usage = 300 };
List<MeterData> expected = new List<MeterData> { meter01, meter02, meter03 };
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ @"c:\meter01.xml", new MockFileData(Meter01) },
{ @"c:\meter02.xml", new MockFileData(Meter02) },
{ @"c:\meter03.xml", new MockFileData(Meter03) }
});
MeterDataRepository meterDataRepository = new MeterDataRepository(mockFileSystem);
// act
List<MeterData> actual = meterDataRepository.GetMeterDataFromFiles(folder, fileNames);
// assert
CollectionAssert.AreEquivalent(expected, actual);
}
The
values for Meter01, Meter02, and Meter03 are constants defined elsewhere in my
test class.
As
you can see, System.IO.Abstractions makes it pretty easy to mock the file
system and produce a test which truly doesn’t have a dependency on the real
file system.
An
additional benefit is that it’s pretty easy to check all this into Team
Foundation Server’s source code control (or whatever source control system you’re
using) – there’s no need to add check in some sample files, a batch file that
moves stuff around, and a README file that explains how to set up everything.
System.IO.Abstractions
is available via NuGet.
Update: In reviewing my notes on this topic, I remembered Jonathan Channon's excellent post, Abstracting the File System. Check it out.
Update: In reviewing my notes on this topic, I remembered Jonathan Channon's excellent post, Abstracting the File System. Check it out.
Comments
Post a Comment