- Create tests that use times relative to "now".
This means that we do things like "DateTime.Now().AddMinutes(10)" to get a time 10 minutes in the future. Unfortunately, this is not always practical in our tests.
- Change our calls to "DateTime.Now" so that they instead call some sort of time provider that we can swap out for testing.
Note: The code for this project is available on GitHub: https://github.com/jeremybytes/house-control. You can see how this code was created by following along with the articles collected here: Rewriting a Legacy Application.
Since this application deals with scheduling, it does have calls to "DateTime.Now". And I've put together several unit tests using Option #1 (creating relative time records), but I've hit the limit on that. In order to adequately test all of the functionality -- which is crucial to moving forward confidently -- I need a way to use an arbitrary time for "now" in the unit tests. So, it looks like we need to move to Option #2.
Looking for Solutions
To try to find a good solution, I started with StackOverflow (as many developers do). I came across a very good answer by Mark Seemann, and he also points to an article that he wrote several years back: Testing Against the Current Time.
These solutions seemed a bit more complicated than I wanted to undertake, and they had some functionality that I didn't need. So I took a slightly different approach. I centralized all of the calls to "DateTime.Now" and then set up a property that uses a time provider interface. This property could be swapped out with a mock object for testing and we could use a real time provider as a default to be used in production runs.
Let's take a look at this code. For this, we'll be walking through a few different commits from the GitHub project.
Finding Calls to DateTime.Now
The first step was to find the calls to "DateTime.Now" in our code and see what we can do to centralize those calls. This code is taken from commit 956aab2 (before making these changes). You don't need to follow along with the source code to see what's happening here, but it's available just in case you'd like to.
First, I found some comparisons between the event time and DateTime.Now:
These are both checking to see if a time is in the past (okay, the second is technically checking that something is *not* in the past, but we can flip the "if" statement around).
Next, I found a method that checks to see how far the event time is from DateTime.Now:
To centralize the calls to "DateTime.Now" I decided to create some extension methods for this functionality. This was pretty easy to do. I already had a "ScheduleHelper" class that had several static methods in it. So all I had to do was add the new methods and make the class itself "static".
If you're following along, this code is in commit 6523bc0.
For the "DurationFromNow" method, I basically just moved the existing method from the "HouseController" class, made it static, and added the "this" keyword to the parameter. For more information on extension methods, check out the article & video: Quick Byte: Extension Methods.
Then I added a new method that would check to see if the time is in the past. Here's how we used this new method in the code:
Since "IsInPast" is an extension method, we can treat it as if it is a method directly on "DateTime". The only other real change to the code is that we inverted the "if" statement in the "RollForwardToNextDay" method so that it can use the "IsInPast" method.
Creating the Time Provider
Now that our calls to "DateTime.Now" are all in one spot, it is easier to swap them out for calls to another object. For this, I created a very simple interface: ITimeProvider.
These updates are in commit ece190d.
Then we need an implementation that we can use in production. This is pretty simple:
This just calls "DateTime.Now". Seems like a bit of an indirect route to get to "now". And it is. We're adding a layer of indirection so that we'll have something we can swap out for testing.
Using the Time Provider
Our calls to "DateTime.Now" are all in the "ScheduleHelper" class. This means that we'll want to use our new time provider in this class. Since this is a static class with static methods, I decided to add a static property that we could use. Here's the property:
The reason the property is set up this way is so that we can use property injection. (For an overview of property injection, check this article: Dependency Injection: The Property Injection Pattern).
The short version is that if we do nothing, this code will use our "CurrentTimeProvider" object (which makes actual calls to "DateTime.Now"). This is what we want when we run our application. But for testing, we can set this property to a fake or a mock time provider.
With the new property in place, we just need to update the places where we were calling "DateTime.Now" so that they use the time provider.
So, our extention methods both use the "TimeProvider" property instead of making calls to DateTime directly.
A Call to DateTime.Today
I was so busy looking for calls to "DateTime.Now" that I missed a call to "DateTime.Today". This is in the "Schedule" class.
When we're loading up the schedules from the CSV file, we want to use today's date. This will cause us problems for our testing just like calls to "DateTime.Now" does.
Instead of adding a new method to the "ITimeProvider" interface, I decided to add a couple more methods to the "ScheduleHelper" class since methods from this class are already used throughout the application.
Now we can update the call in the "Schedule" class:
Since the "ScheduleHelper" uses the time provider, we'll get the results that we expect regardless of whether we are running our application or running our unit tests with a mock time provider.
Mocking Current Time in Unit Tests
With all this code in place, we haven't changed the behavior of our application (hopefully). But we have given ourselves an injection point that we can use in our unit tests.
Previously, I hit a roadblock in the unit tests when Option #1 (above) was no longer adequate for tests. Because of that, I just left a placeholder to remind me that I had some more work to do (from "ScheduleHelperTests.cs"):
Now that we have our time provider in place, we can create a real test:
For this test, we want a "current time" of January 12, 2015 at 4:35:22 p.m. (We'll take a look at "SetCurrentTime" in just a bit to see how this is set.)
Then we create a record that is "in the past" based on the current time: January 12, 2015 at 3:32:00 p.m. When we roll this to the next day, we'll expect that the value is January 13, 2015 at 3:32:00 p.m. And that's what the rest of this test checks for.
Here's our "SetCurrentTime" method. This is just a method that we have inside our unit testing class.
For this, I'm using Moq as a mocking framework. The first line creates a new mock object based on our interface "ITimeProvider". Then in the "Setup" method, we say that when someone calls the "Now" method on our mock object, we want to return the "currentTime" value -- which happens to be the value that is passed in as a parameter to this method.
Once we have our mock object set up, we assign it to the "TimeProvider" property of our "ScheduleHelper". Now any calls into the "ScheduleHelper" will now use our mock time provider.
A Bit of a Problem
When we run all of our tests, we run into a bit of a problem:
One of our tests fails. And notice, this is not the test that we just modified; this is a completely different test.
Let's re-run just the failed test to see if we can figure out what's wrong:
Now the test passes! Crap. This means that we have some kind of interaction between our tests.
This turns out to go back to our TimeProvider property:
Our property is "static". This means that when we run our unit tests, we only have *one* TimeProvider that's shared with all of our calls. So if one of our tests sets the property to something different (like our new test does), it retains the value for other tests.
I spent quite a bit of time going over different possibilities to fix this. We could make the TimeProvider an instance property rather than a static property, but that would upset a lot of the other code (we may even need to move it to a different class).
A (Not Perfect) Fix
Since this is only causing problems in our tests, I opted for a different solution. At the top of the test classes, I added a "Setup" method that would put the "TimeProvider" back to its default value:
I added this "Setup" method to both the "ScheduleTests.cs" and "ScheduleHelperTests.cs" files. This method runs before every test. It will reset the time provider, and then we can use the "SetCurrentTime" method if we need to override it to something else.
With this in place, we can run all of our tests, and they complete successfully:
This is not an ideal solution, but it works for our current situation. The problem? This is not thread-safe. We could see a problem in a multi-threaded test environment. If our tests kicked off simultaneously on multiple threads, then we could end up with some incorrect values for our static "TimeProvider" property (since there would only be one that is shared across multiple threads).
This is not something that we need to worry about with our current test runner for MS Test, but this could change in the future. In that case, we probably need to go back and re-work the time provider a bit (probably by going to one of Mark Seeman's proposed solutions).
This is a fairly simple way that we can mock the current time for testing purposes. We were able to centralize our calls to "DateTime.Now", so that made things a bit easier. Then we created a simple interface that we could use as a seam in our code. With this in place, a property allows us to inject a mock implementation for our tests. But if we do nothing (like when our application runs), we'll use the actual time from the "CurrentTimeProvider" object.
I'm always a bit cautious about adding complexity to my applications. There's still something about this solution that doesn't quite sit well with me, but I'll keep working on it.
This definitely gives me the chance to create a whole slew of new unit tests. These will be vital for moving forward with some of the functions I have in mind (like implementing the weekday/weekend schedules).
Now I have a pretty good starting point. Refinements will come as the project progresses. Writing software is a process, and we often learn what works and what doesn't work as we go. The important bit is that we keep learning from our experiences.