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.
Last week, I added some functionality to filter time segments by start date. Since time trackers generate many time segments over time, it’s not practical to show all of them. The list view would slow down over time.
As always, adding new functionality provides an opportunity to learn more about how the Time Tortoise technology stack works, and I covered some of that last week. However, there’s an inherent conflict between investigating new aspects of the stack, and writing unit tests first. While writing unit tests can help clarify a design, getting a new feature to work sometimes requires some initial experimentation. Integrating this with a test-first approach can lead to a lot of re-writing.
For example, consider the Entity Framework code changes from last week. Because they modified the way time segments are loaded, they required numerous changes to unit tests. But the necessary changes were only apparent after some experimentation with the EF code.
As a result, this week has been focused on fixing unit tests, and adding new ones. Here are some unit test topics from this week’s work.
Unit Tests vs. Integration Tests
Data access code is tested in two ways in Time Tortoise:
- Using mocks: The Entity Framework code for Time Tortoise lives in the Data Access Layer, which is encapsulated in a project called
TimeTortoise.DAL. The view model accesses the DAL through the Repository class. But in unit tests, we don’t want the DAL code to run, since that would require a database, which is more infrastructure than we want for a unit test. Therefore, we need a mock Repository that returns known values.
- Using an in-memory database: The mock Repository doesn’t actually run any EF code, so unit tests aren’t useful for testing new EF code. However, we still want integration tests to run as quickly as possible. Using an in-memory SQLite database helps achieve that goal.
Since last week’s functionality involved new EF code, I added integration test code to make sure it works as expected. It’s tempting to always use integration tests. Since they use the same code path as the UI, they don’t require much extra work to write. In contrast, unit tests require mocking, along with the interface-based design that allows mocking to work best. But that’s actually a benefit of unit tests, since the design that allows them to work is good design. So only use integration tests when you have to.
When you’re using a mock repository, there’s no EF code running during the test. So you can’t rely on EF to return results. Instead, the test has to supply its own results, and has to ensure that those results make sense.
The moq framework is designed to take an interface (which doesn’t contain any executable code) and return an object whose methods you can call from a test. To specify what those methods should return, you use
Returns. For example:
var mockRepository = new Mock<IRepository>(); mockRepository.Setup(x => x.LoadActivities()) .Returns(Helper.GetActivities());
GetActivities is a helper method that returns a list of test activities.
Things get a bit more complex when you need to set up a method that takes parameters. You can pass literal parameters as part of
Setup, but then the specified return value is only returned when the parameters match. Otherwise you’ll get a
null return value.
If it doesn’t matter what tests pass in as parameters, you can use the
It.IsAny method. The LoadTimeSegments repository method takes parameters for filtering purposes. They could be set up as follows:
mockRepository.Setup(x => x.LoadTimeSegments( 1, It.IsAny<IDateTime>(), It.IsAny<IDateTime>() )).Returns(Helper.GetTimeSegments());
This setup specifies that the
activityId parameter must be
1, but the start and end parameters can be any valid
IDateTime values. This allows this setup to be used by multiple tests, rather than requiring each test to set up its own parameters.
Repeating Test Code
Some unit test proponents argue that they should be able to read a test from beginning to end without jumping around in the source. This implies that each test should set up its own context without using any helper methods.
While it’s true that a test without function calls may be easier to read, it’s harder to maintain code in which many tests repeat the same lines of setup code. If any of that code needs to change, it has to change in multiple places. There’s no argument about this principle for production code, and I think it makes sense for test code as well.
This week, I continued to expand the new
Helper test class, which contains code that creates test activities and time segments, along with context and connection objects for database access.
In an earlier post on test-related topics, I explained the value of depending on
IDateTime rather than
SystemDateTime class, an implementation of
IDateTime, originally contained just one line of code:
public DateTime Now => DateTime.Now;
Consumers that needed the system clock date and time could call
SystemDateTime.Now, while test code could set up a mock
IDateTime, setting the
Now value to any convenient value.
This is useful, but in some cases it’s also necessary to set an instance of
SystemDateTime to a particular date/time value. I have now added a constructor for that purpose, along with a
Value property to retrieve the set value.
SystemDateTime is still a very small class, and gets most of its functionality from the
DateTime instance contained in the
In some cases, test code is able to exercise a class in ways that are not possible through the user interface. For example, it doesn’t make sense to add a time segment if there is no selected activity, since a time segment can’t exist in isolation. In the UI, this requirement is enforced by enabling and disabling buttons as list view rows are selected and unselected.
But unit test code doesn’t go through the View, so it isn’t bound by these UI-level restrictions. I find that throwing exceptions is the right option in these cases. It truly is an exceptional code path, since the user should never be able to reach it. Unit tests can verify the scenario by setting up the prerequisites and ensuring that the correct exception is thrown. And if a regression in the UI results in a situation where users are able to get themselves into an invalid condition, the exception message is there to remind the developer that something needs to be fixed.