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.
Over the past few weeks, I have been implementing time segments. Last week was about editing time segments. This week, it’s adding and deleting, with some digressions on unit testing.
Adding and Deleting
Although a time tracker provides a convenient way to record when work starts and stops, sometimes you’ll forget to click that Start/Stop button at the appropriate time. So a time tracker needs to provide a way to add time segments to an activity after the fact, with start and end times entered manually rather than read from the system clock.
Similarly, you might add a time segment (either manually or through the start/stop approach), and then later decide that you didn’t actually do any work during that time period. For that scenario, there’s the Delete button.
As described in past weeks, one activity can have many time segments. Entity Framework infers this relationship from the Time Tortoise model, in which Activity
has a List<TimeSegment> TimeSegments
property, and TimeSegment
has an int ActivityId
property. Also required are two properties of the Context
class, DbSet<Activity> Activities
and DbSet<TimeSegment> TimeSegments
.
With these and other prerequisites in place, we’re ready to add and delete time segments.
Add
The Add button under the time segment list in the UI calls AddTimeSegment
in the main view model. The view model then creates a new TimeSegment
and a new TimeSegmentViewModel
. The model object gets added to a list that Entity Framework uses to keep track of time segment data. The view model object gets added to an observable collection that the UI uses to display the list of time segments.
Saving the new time segment happens seamlessly. Calling DbContext.SaveChanges
saves it along with any other changes that Entity Framework is tracking.
Delete
The Delete button under the time segment list in the UI calls DeleteTimeSegment
in the main view model. The view model then removes the time segment from the time segment observable collection, and calls DbContext.Remove
to inform Entity Framework of the change.
Calling DbContext.SaveChanges
deletes the segment from the database.
A Reminder About Binding
A time segment has a start time and an end time, which can be used to calculate the duration of the segment. These three values are displayed in the time segment grid, and the two time values are also displayed in editable text boxes below the grid. The grid and the text boxes are supposed to stay in sync through XAML data binding.
However, I noticed this week that when I updated the values in the text boxes, the grid didn’t update. To get the grid to update, I had to save and reload the time segments.
Inside my <Grid>
tag in the main page XAML, my binding was set up as follows:
<TextBlock Grid.Column="0" Text="{x:Bind StartTime}" />
<TextBlock Grid.Column="1" Text="{x:Bind EndTime}" />
<TextBlock Grid.Column="2" Text="{x:Bind Duration}" />
But the correct binding configuration is this:
<TextBlock Grid.Column="0" Text="{x:Bind StartTime, Mode=OneWay}" />
<TextBlock Grid.Column="1" Text="{x:Bind EndTime, Mode=OneWay}" />
<TextBlock Grid.Column="2" Text="{x:Bind Duration, Mode=OneWay}" />
Notice that each binding now specifies a Mode
value. When the mode is omitted, it uses a default mode. In this case, the default is OneTime. This mode means the binding target (the list view) only gets updated “when the application starts or when the data context changes.” This isn’t appropriate if the user will be modifying data on the fly.
My takeaway from this bug is that one should always specify the binding mode, to avoid relying on a default value that may not be appropriate. It would be nice to be able to turn off the default and require an explicit mode.
Thoughts on Unit Tests
The binding bug just described is a type of bug that unfortunately can’t be detected through unit tests, since it only affects the View, which is not covered by unit tests. However, most of my changes from this week can be verified using automated tests.
Exceptions and code coverage
I have written before about the code coverage game, the object of which is to get to 100% code coverage. The problem with this goal is that the last few percentage points (or tenths of points) can take a lot of time to achieve. In some cases, it may simply not be possible to get them. For example, one of my tests uses this xUnit.net syntax:
var exception = Record.Exception(() => mvm.StartStop());
This code stores the exception type that is thrown when the StartStop
method is called, or null
if no exception is thrown. It’s a convenient way to verify code that should throw exceptions under certain conditions.
Unfortunately, the code coverage system doesn’t behave the way one might want when it encounters this code. Rather than reporting that the line successfully ran, it marks it as “partially covered,” which translates to less than 100% total coverage. Presumably, the code coverage logic notices the exception, which is an interruption in the flow of execution, and assumes that some code must not have executed. Hence the partially covered result.
One workaround for this issue is to simply not run code coverage on unit test projects. But that could hide useful information about test code that isn’t getting run for one reason or another. Instead, it’s better to selectively exclude tests that record exceptions, using one of the exclusion options explained in my code coverage article. That’s what I did this week: create an ExceptionTests
class for that type of test, and exclude it from coverage.
100% coverage
Before I moved my exception test to its own class, my code coverage result was more than 99%, but less than 100%. So when I added new tests and new code, I couldn’t tell right away whether all of my new code was covered. I had to drill into the code coverage report and verify that the missing coverage was just due to the exception test. Now that the exception test is excluded, I can tell at a glance whether I’m missing any tests, which is the only reason why my coverage would drop below 100%.
Now that I’m using this approach, I’m starting to think of the coverage number as a binary condition rather than a percentage. Just as even one failing unit test indicates a problem, maybe anything less than 100% coverage should mean either missing tests, or code that needs to be excluded from coverage measurement.
Code coverage in the workplace
The topic of code coverage came up at work recently. In my group, unit tests are highly valued, but code coverage measurement is not. The reason for this is common to any measurement enforced by a manager: you get what you measure. If you measure code coverage, you might get it at the expense of good tests, as developers who are trying to hit their deadlines find creative ways to get to the target number. A more complicated (and expensive) measurement may be required to ensure that developer are producing the kinds of tests that you want — i.e., tests that cover the most important scenarios.
But just because it’s bad for a manager to track a metric doesn’t mean it’s bad for an individual to track the same metric. A programmer can use code coverage metrics to inform their own work, without feeling like they are doing it just to create a number on someone’s status report. Similarly, most professionals would feel insulted if they were asked to provide detailed accounting of every minute of their day. But tracking your own time can be a useful way to make sure you’re working effectively.