Table of Contents

Writing testable code

Below, you can find how applying the concept of dependency injection and the use of interfaces is useful to create testable code.

Refactoring code for better testability

Consider the following example of defining a path cache. This class allows you to add paths, and the cache will hold the rewritten paths. You can inspect the cache using the CachedItems property.

public sealed class PathCache
{
    private readonly HashSet<string> cachedPaths;
    private readonly PathRewriter pathRewriter;
    public PathCache()
    {
        pathRewriter = new PathRewriter();
        cachedPaths = new HashSet<string>();
    }
    public void AddPath(string path)
    {
        string rewrittenPath = pathRewriter.RewritePath(path);
        cachedPaths.Add(rewrittenPath);
    }
    public IEnumerable<string> CachedItems
    {
        get { return cachedPaths.ToList().AsReadOnly();}
    }
}

Now a unit test is needed in order to verify that this class is working correctly. Suppose you write the following test:

[TestMethod()]
public void AddPath_ValidPath_PathPresentInCache()
{
    // Arrange
    PathCache cache = new PathCache();
    string path = @"Visios\Customers\Skyline\Protocols\Test";
    string expectedPath = @"V\C\Skyline\P\Test";
    // Act
    cache.AddPath(path);
    // Assert
    CollectionAssert.Contains(cache.CachedItems.ToList(), expectedPath);
}

There is a problem with the test above. It is implicitly also testing the Rewrite method of the PathRewriter class. This means that in case the Rewrite method of the PathRewriter class does not work as expected, this unit test will also start to fail, even though the logic you are trying to test (which is adding items to the cache) might be implemented correctly. This is something that should be avoided.

The root cause of this problem is that there is a fixed dependency on the PathRewriter class. In the PathCache constructor, you can find the following:

public PathCache()
{
    pathRewriter = new PathRewriter();
    cachedPaths = new HashSet<string>();
}

A way to overcome this is to use dependency injection. The following refactoring makes use of dependency injection through the constructor:

public PathCache(PathRewriter rewriter)
{
    pathRewriter = rewriter;
    cachedPaths = new HashSet<string>();
}

Now you can provide or inject an instance of the PathRewriter class. However, you are still injecting the PathRewriter class. Ideally, you need to have more freedom on what you can pass along to avoid implicitly testing other code.

To achieve this, you can introduce an IPathRewriter interface (in Visual Studio, you can do this by putting your cursor over the PathRewriter class name, right-clicking, and selecting Quick Actions and Refactoring in the context menu. This will bring up another context menu where you can select Extract interface:

public interface IPathRewriter
{
    string Rewrite(string path);
}

Now you can add the following to indicate that the PathRewriter class implements this interface:

public class PathRewriter : IPathRewriter

Now update the PathCache class so it has a dependency on IPathRewriter instead of PathRewriter.

public sealed class PathCache
{
    private readonly HashSet<string> cachedPaths;
    private readonly IPathRewriter pathRewriter;
    public PathCache(IPathRewriter rewriter)
    {
        pathRewriter = rewriter;
        cachedPaths = new HashSet<string>();
    }
    ...
}

These changes allow you to now create a unit test for the PathCache class that no longer depends on the PathRewriter class:

[TestMethod()]
public void AddPath_ValidPath_PathPresentInCache()
{
    // Arrange
    string expectedPath = "rewrittenPath";
    Mock<IPathRewriter> pathRewriter = new Mock<IPathRewriter>();
    pathRewriter.Setup(p => p.Rewrite(It.IsAny<string>())).Returns(expectedPath);
    PathCache cache = new PathCache(pathRewriter.Object);
    string path = @"Visios\Customers\Skyline\Protocols\Test";
    // Act
    cache.AddPath(path);
    // Assert
    CollectionAssert.Contains(cache.CachedItems.ToList(), expectedPath);
}