Skip to main content

My New Years Resolution - Use more shims

When I need to unit test my code in isolation, I typically use Moq to mock the interfaces that aren't related to the methods I'm testing. Until today, that is. I wanted to test a little bit of functionality that depends on values coming back from the System.IO namespace in .NET 4.5. Specifically, if a file in a given folder was more than a certain number of days old, it would get deleted. How do you unit test that? Here is the code that needs to be tested.

public void Rotate()
{
    var searchPattern = "*." + BaseName;
    var files = Directory.GetFiles(Folder, searchPattern);
    foreach (var file in files)
    {
        if (File.GetCreationTime(file) < DateTime.Now.Add(-MaxLogAge))
        {
            File.Delete(file);
        }
    } }

In the past, I probably wouldn't have bothered testing it. After all, it's pretty straightforward. Today I was feeling extra cautious.

The first problem with testing this is that Directory.GetFiles() is going to return me an array of files matching my search pattern. Unless I create some files, it will return an empty array. Next, File.GetCreationTime() will return the actual DateTime value. The only saving grace is that MaxLogAge is a user-specified TimeSpan value. Finally, do I really want to be deleting files in my unit tests? I've done worse in my career.

In fact, the first thought I had was to create a file in my test folder, set the MaxLogAge value to something like TimeSpan.FromMilliseconds(1), and check that the file really gets deleted. It would probably work, but it seems very inelegant, and possibly ineffective on a really fast machine.

Searching online  yielded a lot of opinions and suggestions, most of them involving abstracting System.IO. The idea is to create an interface containing just the functions I need, and then inject that into my class being tested. The concrete implementation would simply be a thin layer of code wrapping System.IO. During tests, I would continue to use Moq to provide a fake version. I considered it for awhile, and then remembered about the Fakes available in Visual Studio.

I had read about the Microsoft Fakes Assemblies before, but never gave them much thought until now. If you don't know about them, open a C# unit test project and expand the references folder. Now right-click an assembly and look for a context menu item called Add Fakes Assembly. Selecting this item will create a special wrapper for you around that assembly. Basically, you can intercept and change what happens when other code calls into these assemblies. Yes, it's as dangerous as it sounds, but there are some safeguards built in.

The most important safeguard is that your changes vanish when the fake context is disposed. Microsoft recommends wrapping the context inside a using statement, guaranteeing that your changes won't last outside of your tests.

Using the Fakes assemblies, I was quickly able to re-implement three System.IO methods so that I could test my logic in total isolation. The way I did that was with the following three lines of code.

System.IO.Fakes.ShimDirectory.GetFilesStringString = (s, p) => new[] { "2014-01-01.myapp.log" };
System.IO.Fakes.ShimFile.GetCreationTimeString = s => DateTime.Now.AddDays(-rotator.MaxLogAge.TotalDays + 2);
System.IO.Fakes.ShimFile.DeleteString = s => isDeleted = true;

Notice the namespace and class names. System.IO.GetFiles (the overload that takes two strings) is named System.IO.Fakes.ShimDirectory.GetFilesStringString(). I simply pass in a lambda expression that returns what I want the built-in method to return, and it does it. That's really all there is to it. I changed the logic to those three methods and called my method. If I did it correctly, my code would try to delete the file, which would simply set a flag I could check later.

The entire test method is here.

[TestMethod]
public void RotateRemovesFilesOlderThanDefaultMaxAge()
{
    using (ShimsContext.Create())
    {
        // Arrange
        var isDeleted = false;
        var rotator = new LogRotator();
        System.IO.Fakes.ShimDirectory.GetFilesStringString = (s, p) => new[] { "2014-01-01.myapp.log" };
        System.IO.Fakes.ShimFile.GetCreationTimeString = s => DateTime.Now.AddDays(-rotator.MaxLogAge.TotalDays - 1);
        System.IO.Fakes.ShimFile.DeleteString = s => isDeleted = true;
 
        // Act
        rotator.Rotate();
 
        // Assert
        Assert.IsTrue(isDeleted);
    }
}

Notice that I create a ShimsContext inside of the using statement, and include the entirety of the test inside the block. This ensures that my changes vanish when the test ends, and prevents those changes from leaking into any other test code accidentally.

The one snag I ran into was matching the .NET method to the appropriate Fakes method. At first, I used GetFilesString instead of GetFilesStringString, and couldn't figure out why it wasn't returning my array. After I got past that hurdle, the rest was easy. So you need to be careful, and understand what method you're really modifying.

I know there are many people who think that shims are evil and should be avoided at all costs. I am not one of those people. Used properly for testing, they solve a very specific problem in a very simple way. That said, if I ever saw the Fakes namespace in production code, I would consider some very harsh and severe punishment for the developer who added it.

I hereby make a New Year's Resolution that I will continue to explore shims, along with the rest of the Microsoft Fakes system. This is a resolution I think I can keep.

Comments

Popular posts from this blog

How to copy your Frozen Free Fall progress to a new phone

It's happened to all of us. You are about to get a branch new smartphone, when it hits you. You're on level 250 of Frozen Free Fall. If you get a new phone, you'll lose all of that progress! Ok, admittedly this isn't the most pressing problem of our time, but it's annoying. So today I decided to do something about it.

I have a Samsung Galaxy S4 (Android), and just received an iPhone 5c. Before you bash me on my phone choice, let me explain that the iPhone is provided by my employer at no cost to me. Now you may proceed to bash me for putting games on my work phone. 
First step: Frozen Free Fall had already been installed on both devices. Next, using the Astro File Manager on my Galaxy, I searched and found the Frozen game save stored in /storage/sdcard0/Android/data/com.disney.frozensaga_goo/files/user.dat. I imagine it will be in the same location on any Android phone. If not, just look for user.dat in a folder with a similar name. So, using a USB cable and the A…

Ionic vs. Bootstrap - for a Web App

Ionic 1.x vs Bootstrap 3.x for a Web AppI was recently asked at work to come up with a comparison between Ionic Framework and a more traditional Angular/Bootstrap combination to create a web app. The application will primarily be used in a desktop web browser (probably Chrome or IE). There are also some use cases where it will be accessed from Safari on an iPad. However, this is purely a web; there are no plans to install the app onto the iPad as a hybrid app. Thus, recommending Ionic to build the UI hadn't occurred to me until the request was made.

This is even more surprising in that I recently published a Pluralsight course on Ionic Framework 1. It should have been the first thing that crossed my mind.
One constraint is that currently only Angular 1.x and Bootstrap 3 are authorized web technologies. Ionic 1.3 was recently approved, but not Ionic 2, Angular 2, or TypeScript yet.
Given those constraints, herein is my attempt at coming up with reasons to use (or not to use) Ionic…

How I Finally Got AdMob and Ionic Framework to Play Nice Together

Note: Some of the plugins referenced in this article have had their names, and possibly their APIs, changed. The information contained herein may no longer work. Please don't rely on it until I have had a chance to review and update it. -Mike Callaghan
TLDRThis is a summary of how to display ads in my MDCL (1650) mobile application, which was written using the Ionic Framework. If you want to see the sample project that displays both banner and interstitial ads, you can look at my AdMob Demo app on GitHub.

AdMob
The first thing that's necessary is to get an AdMob account, which you can create at https://www.google.com/admob/ Ionic Starter App
If you don't already have an ionic app, you can create one with the following command, which will create a folder named myApp in your current folder, and initialize it with an empty application.
ionic start myApp blank



ngCordova
Next, you'll need to install the ngCordova bower package and include that in your project. This script does m…