In the previous blog post of this series, we introduced the use of isolation frameworks. In this blog post, we will explain how applying the concept of dependency injection and the use of interfaces is useful to create testable code.
This blog post is part of a series on unit testing:
1. Introduction to unit testing
2. Creating unit tests using the MSTestv2 framework in Visual Studio
3. Using isolation frameworks
4. Writing testable code
5. FluentAssertions
6. Data-driven tests
7. Test life cycle attributes
8. Using files in unit tests
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 if 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 now allow you to 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); }
More blog posts on unit testing
BLOG
Great article. Dependency Injection rules!