Quick Example of System.IO.Abstractions Use

I just got on a new project and am trying from the get-go to structure all my code so that it's easily testable. Academically, that is a no-brainer. In practice, though, some chunks of code are more difficult to structure so that they're easily testable.

Towards that end, I've been pushing System.IO.Abstractions again. And, to get that going I decided it was time to refresh my examples. In the code below, we see how injecting the file system into the repository ultimately allows us to write testable code. If structured correctly, then our repository requires no subsequent refactoring to run in a unit test environment versus a "real" environment even when dealing with something like the file system.

Let's take a look at our code.

The Console Application

The console application is straightforward. We have a private method (at the bottom) that assembles and returns a mock file system. Next, we perform the following steps:

  1. We feed that mock file system into our repository.
  2. The repository gets gas meter data from one or more files and returns a list of one or more gas meter objects.
  3. We then iterate through the meters, displaying each meter's identifier and gas usage.

Next, our console application repeats the three steps above but with the real file system. Easy peasy, lemon squeezy.

Below is the code for our console application. (See farther down for our repository and gas meter model.)

using System;
using System.Collections.Generic;
using MySystemIoAbstractions.Models;
using MySystemIoAbstractions.Repositories;
using System.IO.Abstractions.TestingHelpers;

namespace MySystemIoAbstractions
{
    class Program
    {
        static void Main(string[] args)
        {
            // instantiate a repository object using a fake file system
            MeterDataRepository meterDataRepository = new MeterDataRepository(CreateMockFileSystem());

            // get a list of meters represented by the files in our fake file system
            Console.WriteLine("From our fake file system...");
            List<MeterData> meters = meterDataRepository.GetMeterDataFromCsvFiles(@"C:\");
            foreach (MeterData meter in meters)
            {
                Console.WriteLine(meter.MeterNumber + ": " + meter.Usage.ToString());
            }

            // instantiate a repository using the default (real) file system
            MeterDataRepository meterDataRepositoryReal = new MeterDataRepository();

            Console.WriteLine("From our real file system...");
            List<MeterData> metersReal = meterDataRepositoryReal.GetMeterDataFromCsvFiles(@"C:\Users\bill.good\Documents\");
            // use the file system to do some work
            foreach (MeterData meter in metersReal)
            {
                Console.WriteLine(meter.MeterNumber + ": " + meter.Usage.ToString());
            }

            // we're done! get the heck out of Dodge!
            Console.WriteLine("\r\nPress any key to quit.");
            Console.ReadKey();
        }

        /// <summary>
        /// Put together a fake file system.
        /// Obviously, we could go crazy here and create subfolders and include
        /// complex files (binary, massive XML documents, Word docs, etc.)
        /// </summary>
        /// <returns></returns>
        static MockFileSystem CreateMockFileSystem()
        {
            var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
                {
                    { @"C:\meter01.csv", new MockFileData(@"1234, 100") }, // meter 1234 reported 100 therms of gas
                    { @"C:\meter02.csv", new MockFileData(@"5678, 125") }, // meter 5678 reported 125 therms of gas
                    { @"C:\meter03.csv", new MockFileData(@"9012, 95") }   // meter 9012 reports 95 therms of gas
                });
            return mockFileSystem;
        }
    }
}


The Repository Class

Our repository class is the object that deals with the file system directly. In other applications, the repository might deal with a database but in this example (because we're talking about file systems) we're persisting our data on the file system.

Our repository has two constructors. The first takes a file system object, in this case, an interface fro the System.IO.Abstractions library. We use this constructor when we're testing code and want to use an in-memory representation of the file system. The second constructor takes no argument and, as a result, uses the "real" file system. This is the approach you'd use in production, staging, QA, on a development server and, in general, any time you're not unit testing.

Our simple repository only has one other method, which reads files from a specified directory and returns a list of gas meter objects, one for each file found. In a real application, there'd be additional methods, perhaps receiving a list of gas meter objects and then writing out files. But, I think we get the idea of mock versus real file systems from the one method presented below.

using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using MySystemIoAbstractions.Models;
using System.IO;
using System.Xml;
using System.Xml.Serialization;

namespace MySystemIoAbstractions.Repositories
{
    public class MeterDataRepository
    {
        private readonly IFileSystem _fileSystem;

        /// <summary>
        /// The default constructor
        /// </summary>
        public MeterDataRepository()
        {
            _fileSystem = new System.IO.Abstractions.FileSystem();
        }

        /// <summary>
        /// A constructor that will accept an System.IO.Abstractions interface.
        /// </summary>
        /// <param name="fileSystem"></param>
        public MeterDataRepository(IFileSystem fileSystem)
        {
            _fileSystem = fileSystem;
        }

        public List<MeterData> GetMeterDataFromCsvFiles(string folderName)
        {
            List<MeterData> results = new List<MeterData>();

            // get a list of files in the given directory
            List<string> fileNames = new List<string>(_fileSystem.Directory.GetFiles(folderName, "*.csv"));

            // process each file
            foreach (string fileName in fileNames)
            {
                // open the file using a stream
                Stream fileStream = _fileSystem.File.OpenRead(Path.Combine(folderName, fileName));

                // read the stream into a string
                StreamReader reader = new StreamReader(fileStream);
                string myData = reader.ReadToEnd();

                // bust the string into comma-delimited values
                string[] values = myData.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

                // assemble a gas meter using the values and add meter to our results
                MeterData meter = new MeterData();
                meter.MeterNumber = values[0];
                meter.Usage =Int32.Parse(values[1]);
                results.Add(meter);
            }
           
            // get the heck out of Dodge!
            return results;
        }
    }
}


The Gas Meter Class

Finally, we have a class, MeterData, which contains our gas meter. This is a simple class containing only two properties: the meter identifier (a string) and an integer which represents how much gas was used.

namespace MySystemIoAbstractions.Models
{
    /// <summary>
    /// A gas meter with monthly usage attached
    /// </summary>
    public class MeterData
    {
        private string _meterNumber;
        private int _usage;

        public string MeterNumber
        {
            get
            {
                return _meterNumber;
            }
            set
            {
                _meterNumber = value;
            }
        }

        public int Usage
        {
            get
            {
                return _usage;
            }
            set
            {
                _usage = value;
            }
        }
    }
}

Where to Go from Here

This example was a pretty trivial example. If you want to see the original code, then visit the earlier blog posting:



There's also a follow-up post which shows more detail and working with different types of files (binary files, larger XML files, etc.):




And, of course, you'll want to check out Jonathan Channon's excellent post, Abstracting the File System.

You can find System.IO.Abstractions on NuGet and GitHub.


Comments

Popular posts from this blog

List of Visual Studio Keyboard Shortcuts

Using Reference Aliases