👨‍🚀💬

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:

  1. You have to manually remember to reset the DateTime.
  2. 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:

  1. I made _dateTimeUtc [ThreadStatic]. This prevents changes in SharpTime from affecting other threads.
  2. The only way to override the DateTime is by calling a method that returns an IDisposable. This means that changes to the underlying DateTime 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

~ posted in professional on 12.24.2020