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.
Having finished a basic end-to-end idle time feature, I find myself with a bit of a unit test backlog.
Programmers can debate about when to write tests. But in my experience with Time Tortoise, there are times when experimenting first and writing tests later is the only realistic option. During these times, there are already too many technical issues to figure out without throwing testing in the mix as well. That was the case with the recent Time Tortoise Companion work. But now it’s time to pay off the unit testing debt and get back to full coverage.
One of the basic prerequisites for testable code is depending on interfaces rather than concrete classes. For example, Time Tortoise now has a class called
SignalRClient that knows how to talk to the SignalR server in the Time Tortoise Companion app.
MainViewModel takes a dependency on that class so it can receive information about user idle time.
MainViewModel depended on the concrete
SignalRClient class, any unit tests that instantiated
MainViewModel would end up trying to connect to a real Time Tortoise Companion server. This would be less than ideal for at least two reasons: 1) We don’t want to depend on a server being up and running during a test run, and 2) Communicating with a real server would slow down test execution.
Instead, we want
MainViewModel to depend on an
ISignalRClient interface, which
SignalRClient then implements. The
MainViewModel constructor then accepts an
ISignalRClient. Time Tortoise itself can pass in a concrete
SignalRClient that will connect to a real server, while unit tests can pass in a mock client that acts like it is connecting to a server, but doesn’t actually do so.
Code in the UWP Project
A core principle of Time Tortoise design is to minimize the amount of code in XAML code-behind classes (e.g.,
MainPage.xaml.cs), mainly because it’s harder to test. In fact, I don’t write automated unit tests for any of the code in those classes. So one of the tasks during a unit test cleanup week is to see if any code-behind code can be moved to a ViewModel class.
One example of code in this category is timer code. A timer is an essential part of Time Tortoise, but there are technical reasons why some timer code must reside in the code-behind class, not in the ViewModel. For the idle time feature, I took advantage of existing timer code. But over the past few weeks, the code-behind portion of that code got a bit longer than necessary. This week, I moved it to the
MainViewModel class for better testability.
The modern .NET ecosystem benefits from a variety of components that can be assembled to create something new. For example, in addition to .NET and UWP, Time Tortoise uses components like MouseKeyHook, SignalR, and NotifyIcon.
The way to organize these components and their dependencies is with the NuGet package manager, which is integrated with Visual Studio. NuGet is supposed to keep track of dependencies, installing not only the desired component, but the correct versions of all of the components it depends on.
However, this doesn’t always work as designed. Windows DLL dependencies are complicated enough, but the addition of .NET Standard and xUnit.NET seems to make the process more prone to failure. I have run into two problems related to “System.IO.FileNotFoundException : Could not load file or assembly”:
- Unit test discovery: The Visual Studio build process doesn’t seem to copy all necessary NuGet DLLs into the unit test output directories. The workaround is to copy them manually.
- Conflicting dependency versions: This week I ran into the same exception when running unit tests, but the solution is not as simple. The problem is that
Microsoft.AspNet.SignalR.Clientseems to be looking for a version of
Newtonsoft.Jsonthat isn’t compatible with .NET Standard. Both SignalR and Json.NET are supposed to work with .NET Standard, so fixing this may involve some manual version adjustment, since NuGet doesn’t seem to be doing that on its own.