Time Tortoise: Using SystemWrapper for Unit Testing, Part 3

Wrapped Computer

This is one in a series of articles about Time Tortoise, a Universal Windows Platform app for planning and tracking your work schedule. For more on the development of this app and the ideas behind it, see my Time Tortoise category page.

In two previous articles, I wrote about SystemWrapper, a library that makes it easier to test code that depends on .NET system APIs. In this article, I’ll provide more details on how I use SystemWrapper for Time Tortoise testing.

SystemWrapper

To review: One way to make code more testable is to have classes depend on interfaces rather than concrete implementations. That allows unit tests to inject test versions of dependencies, which run faster and have no side effects, while product code can inject concrete implementations of the same dependencies.

One challenge when using dependency injection is that not every class inherits from an interface. For some classes, you only get concrete implementations. This is the case for many .NET Framework classes. SystemWrapper solves this problem by providing a complete set of interfaces, along with implementations of these interfaces that delegate to the underlying .NET types.

SystemWrapperCore

SystemWrapper is based on the classic .NET Framework, while Time Tortoise uses .NET Core. Although .NET Standard 2.0 brings in many classic APIs, it’s not compatible enough that I could just import the SystemWrapper code into a .NET Standard project. Making the necessary changes to all of SystemWrapper would be a lot of work, and I wouldn’t be using most of the interfaces and classes, so I decided instead to convert classes as I needed them. As I use classes in Time Tortoise, I add them to projects in a new solution called SystemWrapperCore.

To fill in some missing unit test coverage in the Time Tortoise DailySummary class, I needed the IFileInfo and IFile interfaces and their corresponding wrappers. For consistency, I decided to adopt IDateTime and its wrapper as well, replacing the one-off DateTime interface and implementation I created earlier this year.

As I convert the SystemWrapper code for use with .NET Standard, I get it to compile by commenting out anything that .NET Standard 2.0 doesn’t support. For example, there’s no .NET Standard version of FileInfo.GetAccessControl, so I commented out that method in the SystemWrapperCore version of IFileInfo and FileInfoWrap. If this method appears in a future .NET Standard release, I can just uncomment it.

In addition to depending on .NET, SystemWrapper interfaces and classes depend on other SystemWrapper interfaces and classes. So another part of the conversion process was to pull in the minimum set of files that were required to get IFileInfo, IFile, and IDateTime to compile. This required a total of twelve interfaces and ten wrapper classes.

Since I haven’t uploaded SystemWrapperCore to nuget.org, I used it in Time Tortoise by compiling its two projects to two DLLs, and referencing those local DLLs.

Using SystemWrapper for Testing

With SystemWrapperCore available, I was able to fill in some unit test gaps involving the class that generates the Daily Summary file. First, I made the following changes to the DailySummary class:

Set Up Dependency Injection

Add IFileInfo, IFile, and IDateTime parameters to the DailySummary constructor. This matches the standard dependency injection pattern that I use elsewhere in Time Tortoise to facilitate unit testing.

Use Interfaces

When declaring variables, replace concrete class types with interface types. For example, the old section of code that created the directory for the daily summary file used the concrete FileInfo class:

var fileInfo = new FileInfo(fullName);
fileInfo.Directory.Create();

In the new code, a _fileInfo variable of type IFileInfo has been initialized in the constructor. So the directory creation process now looks like this:

_fileInfo.Initialize(fullName);
_fileInfo.Directory.Create();

Inject Dependencies

With constructors set up to accept dependencies, callers can now pass appropriate dependencies based on their needs. For example, a unit test that needs to test the file creation process without creating real files can pass a Mock<IFileInfo> to the DailySummary constructor. But the product code needs a real FileInfo to create a real file. However, it can’t pass FileInfo in directly because the standard .NET FileInfo class doesn’t inherit from IFileInfo. Instead, the product code passes FileInfoWrap, the SystemWrapper class that inherits from IFileInfo and delegates its functionality to FileInfo. So when DailySummary.WriteFile is called during a test run, the WriteAllText call that it makes doesn’t do anything other than record that the call happened. But when it’s called while a user is running the app, a real file is written to disk.

(Image credit: cellanr)