Thursday, 13 October 2011

Using DomainTime Instead of DateTime

In a previous post about favouring DateTime.UtcNow over DateTime.Now I mentioned that I don't use the DateTime struct directly in code to obtain the current date or time. Instead I use what I call DomainTime, a wrapper around the DateTime struct. It looks like this:
   public static class DomainTime
{
internal static DateTime OverrideForTesting = DateTime.MinValue;

public static DateTime UtcNow { get { return OverrideForTesting == DateTime.MinValue ? DateTime.UtcNow : OverrideForTesting.ToUniversalTime(); } }

public static DateTime Now { get { return OverrideForTesting == DateTime.MinValue ? DateTime.Now : OverrideForTesting; } }

public static DateTime Today { get { return OverrideForTesting == DateTime.MinValue ? DateTime.Today : OverrideForTesting.Date; } }

public static DateTime MaxValue { get { return DateTime.MaxValue; } }

public static DateTime MinValue { get { return DateTime.MinValue; } }

internal static void Reset()
{
OverrideForTesting = DateTime.MinValue;
}
}

The whole point of using this class is that it makes it easy to test other classes that have some kind of time dependency. To use a contrived example, imagine that you've got a class which will only do its work if the time is between 1am and 2am. It makes the decision (should I work or should I not?) by checking DateTime.Now.

How do you test this? You _could_ run your tests just before 1am, between 1am and 2am, and then again after 2am - but that's just stupid. You should be able to run your tests anywhere, any time, and as many times as you want.

The solution, here, is to make the class depend on DomainTime.Now instead. By doing so, you can
override the current time during testing by setting it with the OverrideForTesting property.

Note that this property is an internal property. Expose it to your test assemblies by using the
[assembly: InternalsVisibleTo("Your.Test.Assembly")]

directive in the AssemblyInfo class in the assembly where DomainTime resides.

Now you can test to your heart's content. It's just a matter of setting the appropriate DateTime for each test.

One final note: The DomainTime.Reset() method is there for your SetUp() or TearDown() methods so that you can avoid having the DateTime set by one test bleed over into another test.

No comments: