Saturday, March 27, 2010

DataWings.IO

    According to the unit test purists, your code should never take a dependency on the System.IO namespace directly. In this way you can stub out the entire file system in your unit tests. Yes, I see the point, and I even agree, but unfortunatly I haven´t actually followed through and done this in my own code. Pure laziness.

    But now I´m working on a new feature in DataWings (no details yet, I´m planning a "Steve Jobs presents the iPad"-like big splash at some time in the future) where one of the main features has to do with manipulating stuff in the file system. So, DataWings now includes a very small assembly called DataWings.IO which at the moment contains just one single static class wrapping the functionality in the System.IO namespace. The entire class definition is listed at the bottom of this post.

    I´m going 100% YAGNI here, and currently only the methods of the System.IO that my exiting new feature needs have been exposed. These are:

  • File.Exists()
  • File.ReadAllText()
  • File.Delete()
  • File.WriteAllText()
  • File.ReadAllLines()
  • Path.Combine()
  • Directory.GetDirectories()
  • Directory.GetCurrentDirectory()

    An interesting note: I´ve implemented everything as extension methods so that you can say things like: string contents = @"c:\myfile.txt".GetAllContents() . I know that some of you cool cats frown at the notion of extensions methods, but I generally think it´s a thing of grace and beauty. Probably my Smalltalk background showing through.

    Another interesting thing: I´ve employed the power of Func<> and Action<> in the code, and IMHO the result is extremly pleasing to the eye: very clean, very easy to understand, very easy to stub the behavior in tests.

    So, as mentioned, the whole point is to make the code testable, and here´s a sample of the kinds of tests you can write if your file system accessing code uses the DataWings.IO functionality instead of System.IO directly.

    Imagine that I've written a class MyCustomBehavior with a method DoSomeStuff(), and an expected side-effect of this method is that a file with known name and known contents is written to the file system. This code takes a dependency on DataWings.IO and not on System.IO. We want to write a test that checks whether this file actually is written. Here's a unit test that tests this:

[Test]

public void DoSomeStuff_FileAndContentsWrittenAsExpected()

{

// Set up

string expectedContents = "Expected contents.";

string expectedFilename = @"c:\contents.txt";

string writtenContents = null;

string writtenFilename = null;

IoExtensions.FunctionGetCurrentDirectory = () => @"c:\";

IoExtensions.ActionWriteAllText = (path, contents) =>

{

writtenFilename = path;

writtenContents = contents;

};

// Test

MyCustomBehavior.DoSomeStuff();

// Assert

Assert.AreEqual(expectedFilename, writtenFilename);

Assert.AreEqual(expectedContents, writtenContents);

}



And here’s the source:






using System;
using System.IO;

namespace DataWings.IO
{
/// <summary>
/// Static class that wraps the functionalty that is found the the System.IO
/// namespace, primarily the static classes File, Directory and Path. All
/// operations against the file system are handled by action/function invocations,
/// and it is possible to set own actions and functions replacing the default
/// ones, giving you the ability to stub out the file system.
/// </summary>
public static class IoExtensions
{
#region Declarations and Static constructor

private static Func<string, bool> _funcFileExists;
private static Func<string, string> _funcReadAllText;
private static Action<string> _actionDeleteFile;
private static Action<string, string> _actionWriteAllText;
private static Func<string, string[]> _funcReadAllLines;
private static Func<string, string, string> _funcPathCombineWith;
private static Func<string, string[]> _funcGetDirectories;
private static Func<string> _funcGetCurrentDirectory;

static IoExtensions()
{
Reset();
}

/// <summary>
/// Resets this static class by setting all actions and functions back to
/// their original values where they access the functionality in the
/// System.IO namespace
/// </summary>
public static void Reset()
{
_funcFileExists = path => File.Exists(path);
_funcReadAllText = path => File.ReadAllText(path);
_actionDeleteFile = path => File.Delete(path);
_actionWriteAllText = (path, contents) => File.WriteAllText(path, contents);
_funcReadAllLines = path => File.ReadAllLines(path);
_funcPathCombineWith = (path1, path2) => Path.Combine(path1, path2);
_funcGetDirectories = directory => Directory.GetDirectories(directory);
_funcGetCurrentDirectory = () => Directory.GetCurrentDirectory();
}

#endregion

#region IO Emulation

public static bool FileExists(this string path)
{
return _funcFileExists.Invoke(path);
}

public static string ReadAllText(this string path)
{
return _funcReadAllText.Invoke(path);
}

public static void DeleteFile(this string path)
{
_actionDeleteFile.Invoke(path);
}

public static void WriteAllText(this string path, string contents)
{
_actionWriteAllText.Invoke(path, contents);
}

public static string[] ReadAllLines(this string path)
{
return _funcReadAllLines.Invoke(path);
}

public static string PathCombineWith(this string path1, string path2)
{
return _funcPathCombineWith.Invoke(path1, path2);
}

public static string[] GetDirectories(this string directory)
{
return _funcGetDirectories.Invoke(directory);
}

public static string GetCurrentDirectory()
{
return _funcGetCurrentDirectory.Invoke();
}

#endregion

#region Setting functions and actions

public static Func<string, bool> FunctionFileExists
{
set { _funcFileExists = value; }
}

public static Func<string, string> FunctionReadAllText
{
set { _funcReadAllText = value; }
}

public static Action<string> ActionDeleteFile
{
set { _actionDeleteFile = value; }
}

public static Action<string, string> ActionWriteAllText
{
set { _actionWriteAllText = value; }
}

public static Func<string, string[]> FunctionReadAllLines
{
set { _funcReadAllLines = value; }
}

public static Func<string, string, string> FunctionPathCombineWith
{
set { _funcPathCombineWith = value; }
}

public static Func<string, string[]> FunctionGetDirectories
{
set { _funcGetDirectories = value; }
}

public static Func<string> FunctionGetCurrentDirectory
{
set { _funcGetCurrentDirectory = value; }
}

#endregion
}
}

No comments:

Post a Comment