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)