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.
One of the preconditions for using test-driven development is a willingness to spend time figuring out how to test your project in an automated way. Just as research and experimentation is required to get your program to do what you want, TDD requires research and experiments that lead to test code.
This week, I have three test topics: testing UI behavior without a UI, using a console application for testing, and testing time-related features.
Testing UI Behavior Without a UI
One of the primary goals of MVVM is to minimize user interface code, which tends to be difficult to test. But ultimately the user cares about how the UI works, not whether all of your View Model tests pass. So if your UI is broken but your tests aren’t, you have to find a way to reproduce the problematic UI behavior in a test. That’s the scenario I ran into this week.
Last week, I added code to support deleting an activity. In proper TDD form, I wrote the tests first, then implemented the code. However, I didn’t implement the View code (i.e., the Delete button). This week, when I added a button and tried it out, I got this error message:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException was unhandled by user code. Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
The Delete
method in MainViewModel
had just two lines:
Activities.Remove(SelectedActivity);
_repository.DeleteActivity(SelectedActivity);
It is supposed to work as follows: remove the selected activity from the in-memory activities list (line 1), and then delete it from the database (line 2). But because of XAML binding, line 1 has side effects. Activities
is bound to a ListView
control. Removing the selected activity from Activities
causes it to also be removed from the ListView
. This means that the activity in question can no longer be selected (since it’s gone). The control handles this by setting its SelectedIndex
property to -1
. This triggers more data binding, in this case an update to SelectedActivity
that initializes it to an empty activity. Therefore, the SelectedActivity
being deleted in line 2 is not the activity that was removed from the list. It’s a new activity with id 0
, and Entity Framework doesn’t know anything about it.
Before we fix this bug, or any bug, TDD requires that we first write a unit test to reproduce it. Since the bug is related to the behavior of a UI control, does that mean we need a UI test to reproduce it? That would work, but UI tests are slower and more complex than unit tests, so we want to avoid them if possible. Is there another way to reproduce the problem?
To reproduce the exact error in question, we need an Entity Framework dependency. Fortunately, we have an in-memory database that we can hook EF up to without slowing down the test much.
To avoid depending on an actual ListView
, we can simulate just the part of the ListView
that responds to row removal. Here’s that simulated ListView
:
mvm.Activities.CollectionChanged +=
delegate (object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
mvm.SelectedIndex = -1;
};
mvm.Activities
is an ObservableCollection
, a type of collection that is normally attached to a control through data binding. In that normal case, the control then listens for events that it’s interested in, like CollectionChanged, which fires “when an item is added, removed, changed, moved, or the entire list is refreshed.” Since we don’t want to depend on a UI control, we instead have a delegate that runs when the event fires. If the change to the collection is a Remove
action, then the delegate sets SelectedIndex = -1
, just as the ListView
would do.
The rest of the test (not shown here) just carries out the steps required to reproduce the bug: create an activity, save it, and then try to delete it. Just as when the steps are carried out manually in the UI, we get a DbUpdateConcurrencyException
. So this integration test successfully reproduces the problem.
As is often the case with TDD, the fix requires less code than the test: just keep a copy of the selected activity. Since the copy is in a local variable, it isn’t affected by the ListView
selection change, so we can use it for the EF delete operation:
var selectedActivity = SelectedActivity;
Activities.Remove(selectedActivity);
_repository.DeleteActivity(selectedActivity);
Re-running the test verifies that this solves the problem.
Although it took some work to reproduce this problem with a test, the test is now available in the future to protect against regressions. It also resulted in knowing a bit more about how ListView
works.
Using a Console Application for Testing
One of the characteristics of a testable component is that it can be used by multiple consumers. One consumer could be a graphical user interface — in this case, a XAML page. Another consumer is the unit test suite, which tries to simulate the graphical UI. But once you have two consumers, it’s not hard to add another one, like a command-line interface.
In the future, it may be useful to have a Time Tortoise command-line interface for end-users. Some actions are more efficiently carried out from the command line than with a mouse or touch screen. But for now, I’m using a console app project just for testing purposes.
The Time Tortoise console app that I added this week is best used for exploratory testing: trying out the view model to see how it works. Since the console app doesn’t have the Assert
functionality of an xUnit.net project, verifying results is a manual process. Therefore, it’s not appropriate for writing tests that will be run regularly.
This week, I used the console app to see which events are fired when activities are added and deleted from the activity list. The output is shown at the top of this post. I used the results of this experiment to write the integration test that I described above.
.NET Core console apps are a bit different from regular .NET console apps. By default, they compile to a DLL rather than an EXE. You then run them using the dotnet command-line tool:
dotnet TimeTortoise.Console.dll
There’s also an option to compile to EXE, which I haven’t tried yet.
Testing Time-Related Features
So far, the Time Tortoise features I have implemented have been fairly basic: creating, retrieving, updating, and deleting activity names in a database. I’m now moving on to time segments: start and end dates and times associated with an activity. For example, I might want the app to record that I worked from 3/7/2017 10:00 PM to 3/7/2017 11:00 PM editing a blog post.
Dates and times are complicated to deal with because the human clock and calendar system is complicated. But even ignoring that, date- and time-related features are inherently problematic for unit tests, because they are often nondeterministic. For example, a basic user story for Time Tortoise is “select an activity and start timing it.” The “start timing” part of that story uses the current date and time. But the current date and time keeps changing. This makes it difficult to write a unit test that asserts against a fixed value.
An effective way to solve this problem is to use dependency injection. This is the same approach that I am using for selecting between real and mock repository classes. MainViewModel
can accept either a mock repository or a real repository in its constructor, because they both implement the same interface, IRepository
.
Similarly, components that need to know the current date and time shouldn’t just call DateTime.Now
, since this would prevent unit tests from getting consistent results. Instead, they should accept an IDateTime
, which could be either a mock date/time object that returns known values, or a standard date/time object that gets its results from the system clock.
There’s no standard IDateTime
interface in .NET. I could take a dependency on NodaTime, which has other advantages in addition to testability. And I may do that at some point. But for now I’m just using a simple IDateTime
interface and a SystemDateTime class that implements the interface by delegating to the standard DateTime
class.