Before I start actually building my time tracking app, I have one more topic to cover: unit testing. I’ll tackle it in two parts, one this week and one next week.
Why Write Unit Tests?
Not every developer believes that unit testing is worth the investment. And among those who support it, there are different opinions about the best unit testing process. My opinion is that it’s best to write test code before production code, and that it’s worthwhile to spend some time planning in advance how a software project will be tested. For UWP apps, it turns out that unit testing isn’t as straightforward as it is with other .NET apps. It’s important to take that into account in the app design.
I have written in the past about how unit testing is useful for learning a programming language. I even suggested using it when writing competitive programming solutions, which is not a context where unit testing normally comes up.
But unit testing is especially important when you have code that you’ll be maintaining for a while. In that case, there are three main benefits of unit testing:
- They promote good design: Writing unit tests, especially writing them before other code, encourages you to think about how the module you’re working on will be used by other parts of your program. The result of this thinking is a program design that makes future changes easier.
- They catch regressions: If you only had to test your program once, you could do it manually. But that’s not the nature of programming. You have to continually test as you make changes, to ensure that you’re not breaking existing functionality. Fast automated unit tests let you run your tests as often as you want, so that you can fix problems as they appear.
- They encourage you to understand your code: When you write and run unit tests, you exercise your code in a very direct way, without running it through a user interface. That experience leads you to understand your code better as you brainstorm tests to throw at it, and debug the ones that behave in unexpected ways.
Unit Testing UWP Apps
As I was experimenting with unit testing my example UWP app, I noticed a couple of limitations:
- When starting a UWP unit test project, a window always pops up, even if the tests are unrelated to the user interface. It appears that UWP unit tests must run in the context of a UWP app.
- Code coverage is not supported for UWP unit tests, despite requests going back a few years.
The first point is mainly just an annoyance. Although it adds a few seconds to test startup time, it only happens once per test run. If you have a lot of tests, it’s not a big percentage of your total test runtime.
The second one is more serious. Although you can benefit from writing unit tests without checking code coverage metrics, it’s not ideal. Here’s the argument: If you follow a test-first approach (write failing tests first and then write the code that makes them pass) your code coverage should be fairly good. But since you’re not perfect — if you were, you wouldn’t need tests — you might unintentionally write code that isn’t covered by a test. A code coverage run identifies code that is missed by your tests for one reason or another.
So that’s what code coverage analysis is for. 100% code coverage doesn’t mean your testing work is done (since covered code could benefit from more tests). But less than 100% code coverage alerts you to code that might need tests.
Solution Design for Testability
After some research and experimentation, I came up with a design that avoids the two limitations described above for most of the code in my solution. It uses three projects:
- A Universal Windows project containing the Views (XAML), associated UI assets, and the application entry point. This project has as little C# code as possible.
- A .NET Standard Class Library project containing the View Model and Model. Most of the application code goes here.
- A .NET Core Application project containing xUnit.net tests. This is the test code for Project #2.
The MVVM pattern is designed for this type of split between View code, which might not be targeted by unit tests, and Model/ViewModel code, which is tested. However, in last week’s example I combined all of the code into one project. I found out this week that to get this test setup to work the way I want, some of the code needs to be excluded from the Universal Windows project.
Design Details
Because UWP is relatively new, some scenarios require a bit of manual work to set up. My unit test design happens to be one of them. Here are the steps I used to get it working.
- Start with last week’s example.
My example app is a UWP application that demonstrates an MVVM design, XAML data binding, Entity Framework, and a SQLite database. All of the code is contained in a single Universal Windows project.
- Open the example solution in Visual Studio 2015, and add a Portable Class Library project called ModelViewModel.
In the Add New Project dialog, choose Class Library (Portable). For Targets, choose Windows Universal 10.0 and ASP.NET Core 1.0. This project has nothing to do with ASP.NET. However, when I didn’t choose that target, I got the following error:
This project requires a Visual Studio update to load. Right-click on the project and choose “Download Update”.
Apparently this error occurs when Visual Studio detects a missing SDK. People have resolved it using various time-consuming steps. But my workaround appears to generate the project I need. Another workaround is to use Visual Studio 2017 RC, but I’m sticking with VS2015 for now.
- Convert ModelViewModel to a .NET Standard Class Library.
In the Project Properties page for ModelViewModel, click Target .NET Platform Standard, click Yes, select .NETStandard1.4 in the drop-down menu, and click Yes again.
.NET Standard is “a set of APIs that all .NET platforms have to implement.” In other words, it’s a set of APIs that you can use without planning in advance which platform you want to target. And for our purposes, it’s a class library that both a Univeral Windows project and a unit test project can reference.
The .NET Standard compatibility matrix indicates that .NET Standard v1.4 is the lowest .NET Standard version compatible with UWP v10.0. I went with that one because I couldn’t get any higher versions to run my example solution.
- Move Model and ViewModel code to the class library.
Move the DAL, Migrations, Models, and ViewModels folders to the ModelViewModel project. Remove the default Class1.cs. Remove all of the Entity Framework references from the UWP project, and add the same references to the MVM project. In the UWP project, add a reference to the MVM project. Clean up namespaces, using
statements, etc. to account for the new source location.
- Add a project for unit tests.
Unit tests generally run with the help of a unit testing framework. The best framework for UWP apps seems to be xUnit.net. (Not to be confused with xUnit, a general category that includes several frameworks). I followed the official instructions for getting started with xUnit.net (.NET Core / ASP.NET Core) to create the third project in my solution, the test project.
The first few steps in the instructions amount to the following: Add a Class Library (.NET Core) project. Replace the default project.json file with one that they provide, which turns the project into a .NET Core Application. Write tests. Run tests. It’s easy once you find the right instructions.
Since the code we’re testing is in the ModelViewModel project, we have to add a reference to that project. I also added a reference to the Microsoft.CodeCoverage NuGet package, to enable code coverage support in Visual Studio.
You can find the code for this example on GitHub at UWP-MVVM-EF-SQLite-3.
Writing Unit Tests
The best time to write a unit test is before you write the code to be tested. Sometimes that isn’t practical. For example, if you’re trying out a new technology to see how it works, it may not make sense to start with tests, since you don’t have enough context about your project. That’s how the current example came about, and it’s why I started with an example app rather than creating my time tracker app right away.
The problem with writing application code first is that it can lead to a redesign when the time comes to write tests. As explained above, I had to create a new project and move some code around when I tried to test my example project.
Here’s the first test I wrote for my example project:
[Fact]
public void Test1()
{
var tvm = new TaskViewModel();
tvm.Load();
}
[Fact]
is an xUnit.net attribute identifying a test that should always pass. The name Test1
needs to be fixed to be more descriptive. And the test doesn’t assert anything. So it needs some work. However, even in its current form, it catches a code bug: when I first ran this test, I got a NullReferenceException
in the Load
method. That’s because of these two lines of application code:
_task = _repository.LoadTask();
Name = _task.Name;
If _repository.LoadTask()
doesn’t find any tasks to load (e.g, because the database is empty), it returns null
, which is a problem when the next line tries to deference the Name
property. So even the most basic of unit tests can help uncover edge cases that might not come up during manual testing.
Despite its usefulness, the other thing to note about this test is that it’s not actually a unit test. That’s because it is testing multiple parts of the application at once, rather than an isolated behavior. Specifically, it’s testing the Load
method of TaskViewModel
, but then that method calls LoadTask
in Repository
, which calls the database via TaskContext
.
A test that exercises multiple components at once is known an an integration test. Integration tests are useful, but they’re not a replacement for unit tests. Unit tests are faster, so you can run more of them more often. And because they target individual components, they can provide clearer results about the behavior of each of those components, not just the system as a whole.
Testing Components in Isolation
If the test function calls Load
in the view model, the view model calls LoadTask
in the repository, and the repository calls the database, how can we avoid having the test function test the database? In other words, how can we write a unit test instead of an integration test?
The answer involves more refactoring, which is another reminder that it’s prudent to write tests first and think about testability early. I’ll go over the details next week.