# Tuesday, October 21, 2008

Have you ever written code that directly used the .NET File APIs? We probably all did although we knew it would make the code less testable and dependent on the file system state. As bad as it sounds, it really requires a lot of discipline and work to avoid this: one would need to create an abstraction layer over the file system, which is not a short task (think long/tedious).

// in how many ways can this break?
public static void CleanDirectory(string path)
{
    if (Directory.Exists(path))
        Directory.Delete(path, true);
    Directory.CreateDirectory(path);
}

Abstraction

Fortunately, there always someone else who got motivated at some point. Ade Miller digged an abstraction of the File System, the IFileSystem interface, that Brad Wilson had written for the CodePlex client project. Very nice since it provides a solid foundation for cleanly abstracting from the File System, and thus increase the testability of our code.

// a little better, testable code at least
public static void
CleanDirectory(IFileSystem fs, string path) { if (fs.DirectoryExists(path)) fs.DeleteDirectory(path, false); fs.CreateDirectory(path); }

Mocking

So with this interface we can write code that we’ll be able to test in isolation from the physical file system. That’s great but there is still a lot of work on the should of the developer: the developer will have write intricate scenarios involving mocks to simulate the different possible configurations of the file system. No matter which mock framework (Moq, Rhino, Isolator, …), he’ll be using, (1) it’s going to be painful, (2) he’ll miss cases. It’s probably easy to write a single “happy path”, but especially with the file system there are quite some realistic “unhappy paths”.

This test case uses Moq to create the scenario where there is a directory already. Although Moq has a very slick API to set expectations, it is still a lot of work to write this basic scenario. (And what exactly is the meaning of “Expect”, the delegate or expression inside, “Returns” and “Callback”?)

[TestMethod]
public void DeletesAndCreateNewDirectory()
{
    var fs = new Mock<IFileSystem>();
    string path = "foo";

    fs.Expect(f => f.DirectoryExists(path)).Returns(true);
    fs.Expect(f => f.DeleteDirectory(path, false)).Callback( () => Console.WriteLine("deleted"));
    fs.Expect(f => f.CreateDirectory(path)).Callback(() => Console.WriteLine("created"));

    DirectoryExtensions.CleanDirectory(fs.Object, path);
}

Modeling

We had our intern, Soonho Kong, work on a Parameterized Model of the File System, built on top of the IFileSystem interface (yes that same interface Brad Wilson published on CodePlex). We say that the model is parameterized because it uses the Pex choices API to create arbitrary initial File System states; Pex “chooses” each such state (actually, Pex carefully computes the state using a constraint solver) to trigger different code paths in the code. You can think of each choice as a new parameter to the test. Or to put this with an example: if your code checks that the file “foo.txt” exists, then the parameterized model would choose a file system state that would contain a “foo.txt” file (or not, in another state, to cover both branches of the program).

So what does it mean for you? Well, the way you write tests that involve the file system changes radically. You simply need to pass the file system model to your implementation. The model is an under-approximation of the real file system (which means that we didn’t model every single nastiness that can occur when the moon is full), but it definitely captures more practically relevant corner cases than we (humans) usually think about. Let’s see this in the following test:

[PexMethod]
public void CleanDirectory()
{
    var fs = new PFileSystem();
    string path = @"\foo";
    try
    {
        DirectoryExtensions.CleanDirectory(fs, path);

        // assert: the directory exists and is empty
        Assert.IsTrue(fs.DirectoryExists(path));
        Assert.AreEqual(0, fs.GetFiles(path).Length);
    }
    finally
    {
        fs.Dir();
    }
}

When we run Pex, we get 7 generated tests. In fact, Pex finds an interesting bug that occurs when a file with the name of the directory to clean already exists. In the Pex Exploration Results window, you can see a ‘dir’-like output of the file system model associated with a particular test case (the fs.Dir() method call outputs that text to the console which Pex captures).

image

This bug is the kind of corner-case that makes testing the file system so fun/hard. Thanks to the parameterized model (and Soonho), we got it for free. Note also that the assertion in our test is pretty powerful since it must be true for any configuration of the file system (it almost smells like a functional specification to me):

// assert: the directory exists and is empty
Assert.IsTrue(fs.DirectoryExists(path));
Assert.AreEqual(0, fs.GetFiles(path).Length);

Happy modeling!

The full source of PFileSystem will be available in the next version of Pex (0.8)..

Tuesday, October 21, 2008 11:16:36 PM (Pacific Daylight Time, UTC-07:00)
Good idea to abstract operations from the file system.
This + a decent library to handle dir/file path (like I try to do with http://www.codeplex.com/FileDirectoryPath) seems to be a must-have for .NET 4.0 .NET Fx.
Since I rely on FileDirectoryPath I saved so much time!
Wednesday, October 22, 2008 7:25:18 AM (Pacific Daylight Time, UTC-07:00)
I had forgotten about that one. We will have to revisit that library.
Thursday, October 23, 2008 2:33:51 PM (Pacific Daylight Time, UTC-07:00)
Hi Jonathan,

This is very interesting. I've struggled a lot with mocking the file system. Too many of our tests end up being so abstracted, we end up testing that certain methods get called and that's it! I mean, we have unit tests with our own IFileSystem interface ... our unit tests end up testing just the interaction between the component and IFileSystem. It gets too abstract.

See my post about this on StackOverflow, Unit Testing Code with a File System Dependency:

http://stackoverflow.com/questions/129036/unit-testing-code-with-a-file-system-dependency

Jonathan, I've been tracking Pex since it showed up on MSR. After seeing this example, I'm seriously considering giving it a test drive at our company. Thanks for posting this example.

One last thing. I can't seem to find Brad Wilson's file system component on CodePlex. Got a link?
Friday, October 24, 2008 10:01:19 PM (Pacific Daylight Time, UTC-07:00)
First the link:
http://www.codeplex.com/CodePlexClient/SourceControl/FileView.aspx?itemId=59623&changeSetId=17983

One would argue that you already have built IFileSystem for other components in your system (Brad did) that this implementation was toroughsly tested (the same assumption you do about the BCL libraries). From there, I beleive it's prefectly fine to use the file system, as just another service.

Like in all problems, these kind of decision should be considered on per project/constraint basis. There's not silverbullet, sometimes a couple integration test are enough to get the job done (but it might come back and bite you later).
Monday, December 01, 2008 10:36:41 PM (Pacific Standard Time, UTC-08:00)
My colleagues at work have also had a need for the IFileSystem interface, and we pretty much did was has been discussed in your article and comments: create a proxy for the File type and generalize with an interface. After some time, we found that we needed to do the same for other resources: DateTime.Now, MSMQ, among others.

I observed that the process of creating the interface and proxy was error prone, and subject to abuse (i.e. tacking on other functionality to IWhatever which cluttered the usage semantics). Consequently, I started to work on some code that would automatically create this interface and proxy for you, given the type and methods you want to generalize. You wouldn't need to write tests for the generated code since you would have a high degree of confidence that code generator is doing the right thing - assuming it is properly tested :).

Feel free to take a look and comment: I've been maintaining the source at http://www.codeplex.com/jolt and plan to update the library in the future.
Tuesday, December 09, 2008 7:55:13 AM (Pacific Standard Time, UTC-08:00)
Thanks for your comment Steve!
Peli
Comments are closed.