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.
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.
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
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
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
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
Set Up Dependency Injection
IDateTime parameters to the
DailySummary constructor. This matches the standard dependency injection pattern that I use elsewhere in Time Tortoise to facilitate unit testing.
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
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:
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
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)