Sunday, 10 June 2012

Shims Shim Shims

Tonight I started a small project and I thought I'd take the opportunity to explore the new Microsoft.QualityTools.Testing.Fakes library. In case you don't know this library basically allows you to "easily" mock out those pesky sealed classes within the .Net Framework, I've only used it to do System.IO tonight but it does do more. I have a feeling though System.IO will be used heavily with it.

What the library allows you to do is to generate a shim version of each sealed class within a dll. So what is a shim? MSDN at the moment says the following:
Shim types allow detouring of hard-coded dependencies on static or non-overridable methods.
What this means to you and I is you get to create a wrapper type, ShimDirectoryInfo for example, you then set delegates for each method and property that you are going to use on the type and let these return your expected values. When your real code then goes to use the original type, DirectoryInfo in our example, it will automatically use the shimmed methods you have defined. What's clever about this is you can not only pass your shims into code by getting the shims generated instance type but you can also declare an AllInstance implementation which any code that creates your type uses. This is neat as you don't have to then modify existing code to take this types.

A simple example that isn't playing with System.DateTime.Now ;) A lot of the examples on the net use DateTime.Now as an example of shims but I wanted to show how I started using them for testing code that relies on System.IO.

I have a simple class LocalStorage that has a constructor that takes a file path. all we do for now in this constructor is check for a null / empty path and whether the directory actually exists, and if it doesn't throw.

 /// <summary>
 /// Creates a new local storage class for accessing file and directory information
 /// </summary>
 /// <param name="rootPath">Base path to storage location</param>
public LocalStorage(string rootPath)
{
  if(String.IsNullOrWhiteSpace(rootPath)){
    throw new ArgumentNullException("rootPath");
  }
  if (!System.IO.Directory.Exists(rootPath))
  { 
    throw new IOException("Path does not exist: " + rootPath);
  }
  _rootPath = new DirectoryInfo(rootPath);
}
Nothing to complicated there but how would we test the directory not existing and returning an exception. Previously I would have done something like pass in a path that "should" never exist however this isn't a particularly elegant way of doing it. So how would shims help.

Here's my new test:

[Fact]
public void NonExistantPathThrowsIOException()
{
  using (ShimsContext.Create())
  {
  //use fakes to say the folder doesn't exist without having to actually hit the operating system
    System.IO.Fakes.ShimDirectory.ExistsString = (path) => false;
    Assert.Throws<System.IO.IOException>(() => new LocalStorage("m:\\test"));
  }
}

To start using Shims after creating the shim types of an assembly, we new up a ShimsContext, ShimsContext.Create(), note how this needs to be in a using statement as it's disposable and for good reason. Shims exist for the lifetime of the appdomain, so if you forget to dispose of your context any shims you have created would be used for all tests until the appdomain was shutdown. Therefore by using a using statement and thus the dispose method you are creating a scope and context.

Now within our context we get to set a delegate against what we want faking. In this case we want to fake our call to Directory.Exists. As this is a static method we can set this globally. This is done by using the ExistsString delegate on the ShimDirectory class. We can use a lambda here to make this really short and sweet.

That's it! Now when we run our test our fake method is used when ever Directory.Exists is called, no more dependency on the real filesystem. Nice :)

Now you can take this quite far and start mocking out further things, GetFiles for example, I want to test that my business logic which trims out specific files that can't be handled by the default GetFiles method.

What you find with methods which take overloads is that when you go to set the delegate you have to ensure you set the correct one. GetFiles for example has the following delegate methods on ShimDirectoryInfo: GetFiles, GetFilesString, GetFilesStringSearchOption. One for each overload combination. It's worth ensure you check the method signature to ensure you get the correct delegate otherwise you'll end up chasing a bug which doesn't exist in the real code but in your test ;)

Finally not everything is implemented via shims, there is still stuff missing. I found that ShimFileInfo doesn't provide a way to fake File write times, but it does names etc. It is a step in the right direction though when dealing with "stubborn" code.

Enjoy



No comments:

Post a Comment