How To Unit Test With DateTime in C#
Your code has compiled. Your unit tests are running. And then you see it. A unit test has failed. After some investigation, you realize the problem: somewhere in the code you have a dependency on something you don't control. The cuplrit? DateTime
.
Occasionally in programs (mostly in unit tests), you'll want some control over the DateTime
returned by the system. There are lots of ways to do this, but I'll go over a couple of ways I don't like before getting to a way I do like.
Option 1: Using Inversion of Control/Dependency Injection
The first option is to use IoC/DI, where instead of relying on DateTime.UtcNow
, you would pass in an interface, like IDateTimer
.
public interface IDateTimer
{
DateTime GetUtcNow();
}
---
public class DateTimer : IDateTimer
{
public DateTime GetUtcNow()
{
return DateTime.UtcNow;
}
}
---
public void LogDate(IDateTimer dateTime)
{
Console.Log(dateTime.GetUtcNow().ToString());
}
In the above instance of IDateTimer
, I'm just using a wrapper around the regular DateTime
(this could be even easier in C# 8.0 with default interface methods). In unit tests, you could mock it out to return a specific DateTime
of your choosing.
This works well, but it potentially requires a lot of injection for something that seems like it should be simpler.
Option 2: Using a static DateTime
Warning: The following code is unsafe.
public class DateTimer
{
public static DateTime? _dateTimeUtc;
public static DateTime UtcNow { get { return _dateTimeUtc ?? DateTime.UtcNow; } }
public static void Set(DateTime dateTimeUtc) {
_dateTimeUtc = dateTimeUtc;
}
public static void Reset() {
_dateTimeUtc = null;
}
}
---
public void LogDate()
{
Console.Log(DateTimer.UtcNow.ToString());
}
public void LogChristmas()
{
DateTimer.Set(new DateTime(2020, 12, 25));
Console.Log(DateTimer.UtcNow.ToString());
DateTimer.Reset();
}
This makes changing the DateTime
simple, but it comes with its own baggage.
Mainly:
- You have to manually remember to reset the DateTime.
- It's not thread-safe. If you change the underlying DateTime, it will do this for all threads, which may lead to unintended and unexpected issues elsewhere.
Option 3: My Preferred Way!
What I like to do has the simplicity of using a static DateTime, but with some safety measures in place to mitigate unforeseen errors and provide thread-safety.
Here's the code:
using System;
namespace SharpTime
{
public static class SharpTime
{
[ThreadStatic]
private static DateTime? _dateTimeUtc;
public static DateTime UtcNow
{
get
{
if (_dateTimeUtc.HasValue)
{
return _dateTimeUtc.Value;
}
return DateTime.UtcNow;
}
}
public static IDisposable UseSpecificDateTimeUtc(DateTime dateTimeUtc)
{
if (_dateTimeUtc.HasValue) throw new InvalidOperationException("SharpTime is already locked");
_dateTimeUtc = dateTimeUtc;
return new LockedDateTimeUtc();
}
private class LockedDateTimeUtc : IDisposable
{
public void Dispose()
{
_dateTimeUtc = null;
GC.SuppressFinalize(this);
}
}
}
}
As you can see, I implemented two main checks:
- I made
_dateTimeUtc
[ThreadStatic]
. This prevents changes inSharpTime
from affecting other threads. - The only way to override the DateTime is by calling a method that returns an
IDisposable
. This means that changes to the underlyingDateTime
will be limited in scope, even if you forget to reset it.
Here's what it looks like in action:
public void LogDate()
{
Console.Log(SharpTime.UtcNow.ToString());
}
public void LogChristmas()
{
using (SharpTime.UseSpecificDateTimeUtc(new DateTime(2020, 12, 25)))
{
Console.Log(SharpTime.UtcNow.ToString());
}
}
That's it! I'm sure there are lots of ways this could be improved, so I encourage you to check out (and fork!) the code on GitHub!
https://github.com/dochoffiday/SharpTime