Are your unit tests failing because the code-under-test is coupled to the system clock? In other words, does the method you are testing use the System.DateTime.Now property, and that dependency is making it hard to properly unit test the code?

This series of posts presents four ways to fake time as a means of improving testability. Specifically, we’ll look at these four techniques:
- The Optional ‘clock’ Parameter
- Brute Force Property Injection
- Inject The IClock Interface
- Mock Isolation Framework
Time Is Bumming Me Out
Time is so rigid. It keeps on ticking, ticking … into the future. The code becomes dependent on the system clock. That dependency makes it hard to properly test the code. Worse, the test code cannot find a lurking bug … until *boom* … the bug blows the system up.
Perhaps an example would be helpful …
public bool Allowed()
{
// Start date's month & day come from settings
var startDate = new DateTime(
DateTime.Now.Year,
_settings.StartMonth,
_settings.StartDay);
// End date is 1 month after the start date
var endDate = new DateTime(
DateTime.Now.Year,
_settings.StartMonth + 1, // This is the lurking bug!
_settings.StartMonth);
if (DateTime.Now >= startDate &&
DateTime.Now < endDate)
{
return true;
}
return false;
}
Notice the lurking bug in this code? Well, if the start month is December then a defect emerges at runtime. The error message you would see looks something like this:
System.ArgumentOutOfRangeException : Year, Month, and Day parameters describe an un-representable DateTime.
This defect is not too hard to notice, if you envision the value in the _settings.StartMonth property is 12. However, the problem with lurking bugs, they go unnoticed until … bang! At some future date (all too often in production) the software hiccups.
In the following code sample, the defect will be found when the continuous integration server runs the tests in December. This is a fairly typical unit testing approach, intended to test the Allowed method.
[Test]
public void Allowed_WhenCurrentDateIsOutsideModificationWindow_ExpectFalse()
{
// Arrange
var startMonth = DateTime.Now.Month;
var settings = new Mock<IModificationWindowSettings>();
settings
.SetupGet(e => e.StartMonth)
.Returns(startMonth + 1);
settings
.SetupGet(e => e.StartDay)
.Returns(1);
var classUnderTest = new ModificationWindow(settings.Object);
// Act
var result = classUnderTest.Allowed();
// Assert
Assert.AreEqual(false, result);
}
You don’t want to go into work on 1-Dec and find that this test is suddenly failing. The test method fails in December because that’s when startMonth is 12 and the defect is revealed. You’ll have a sickening thought, “Wait a second … is it failing in production?”
The production code is not working as intended, and worse than that, the test code never found the issue before the defect went into production.
If you revise the test code to include a few test cases, like the ones shown below, then the boundary conditions are covered and the defect is found every time these test cases are run. However, this test method has another flaw. It cannot pass for all three cases because the code-under-test has a dependency on DateTime.Now.
[TestCase(1)]
[TestCase(5)]
[TestCase(12)]
public void Allowed_WhenCurrentDateIsOutsideModificationWindow_ExpectFalse(
int startMonth)
{
// Arrange
var settings = new Mock<IModificationWindowSettings>();
settings
.SetupGet(e => e.StartMonth)
.Returns(startMonth + 1);
settings
.SetupGet(e => e.StartDay)
.Returns(1);
var classUnderTest = new ModificationWindow(settings.Object);
// Act
var result = classUnderTest.Allowed();
// Assert
Assert.AreEqual(false, result);
}
Before we investigate any further, let’s go back and revise the code-under-test to fix the bug. Here is an improved implementation of the Allowed method.
public bool Allowed()
{
// Start date's month & day come from settings
var startDate = new DateTime(
DateTime.Now.Year,
_settings.StartMonth,
_settings.StartDay);
// End date is 1 month after the start date
var endDate = startDate.AddMonths(1);
if (DateTime.Now >= startDate &&
DateTime.Now < endDate)
{
return true;
}
return false;
}
Imagine that today is Friday the 13th of January 2012. When we run the test cases the first case passes, but the other two fail. We want all three test cases to pass every time they are run.
Fake Time 1: The Optional ‘Clock’ Parameter
Let’s change the code-under-test so that time is provided as an optional parameter. This approach is made possible by .NET Framework 4.0, which allows C# developers to define optional parameters.
public bool Allowed(DateTime? clock)
{
var now = clock ?? DateTime.Now; // If no clock is provided then use Now.
// Start date's month & day come from settings
var startDate = new DateTime(
now.Year,
_settings.StartMonth,
_settings.StartDay);
// End date is 1 month after the start date
var endDate = startDate.AddMonths(1);
if (now >= startDate &&
now < endDate)
{
return true;
}
return false;
}
Now that the code-under-test accepts this ‘clock’ parameter, the test method is modified, as follows. All three test cases now pass.
[TestCase(1)]
[TestCase(5)]
[TestCase(12)]
public void Allowed_WhenCurrentDateIsInsideModificationWindow_ExpectTrue(
int startMonth)
{
// Arrange
var settings = new Mock<IModificationWindowSettings>();
settings
.SetupGet(e => e.StartMonth)
.Returns(startMonth);
settings
.SetupGet(e => e.StartDay)
.Returns(1);
var currentTime = new DateTime(
DateTime.Now.Year,
startMonth,
13);
var classUnderTest = new ModificationWindow(settings.Object);
// Act
var result = classUnderTest.Allowed(currentTime);
// Assert
Assert.AreEqual(true, result);
}
Pros:
- The simplest thing that could possibly work; a KISS approach
- Minimal impact to method callers
- Isolated changes, lower risk
- Testability is greatly improved
Cons:
- Only works with .NET Framework 4
- Method callers are able to pass improper dates and times, invalidating the method’s expected behavior
- Improves testability only one method at a time
- Adds testing-use-only parameters to method signatures
I recommend this approach when working with legacy or Brownfield code, which has been brought up to .NET 4, and a minimally invasive, very isolated technique is indicated.
In the next part of this Fake Time series we’ll look at a brute force, property injection approach.
Like this:
Like Loading...
Related
Pingback: Four Ways to Fake Time, Part 2 « Ruthlessly Helpful