Working against the Clock

Apr 27, 2021

Alternative title: How to be Consistent Time after Time.

Problem of Time

Handling time is an areas that seems straightforward to a new software developer however it usually takes just the first larger project to realize how many pitfalls exist when trying to work with the clock.

Shortly after Hello World one of the first exercises a new developer will write is a statement similar to the following:

DateTime local = DateTime.Now;
Console.WriteLine(local);

Simple.

However, it doesn't take a lot of experience to recognize that the local time can represent one of the 35+ current time zones and is not as useful as a unified single view of time.  This is especially true in the current cloud deployment models where you have no control over the local time of the servers. Fortunately a singular view of time exists in the form of the Coordinated Universal Time (UTC) . This is solved by using the following code:

DateTime utc = DateTime.UtcNow;
Console.WriteLine(utc);

In recent projects I have started to favor the DateTimeOffset class in c# to help enforce have a common UTC view of time. To compare DateTimeOffset vs. DateTime:

DateTimeOffset.Now;			--> 4/27/2021 7:49:04 AM -04:00
DateTimeOffset.UtcNow;		--> 4/27/2021 11:49:04 AM +00:00
DateTime.UtcNow;			--> 4/27/2021 11:49:04 AM
DateTime.Now;				--> 4/27/2021 7:49:04 AM

Test of Time

Testability of time dependent code becomes a challenge when you use the static implementations of time as above. Frequently to create quality unit or integration tests you need to control the concept of time. E.g. an auction bid expires after a certain time period. To test you need to create the bid and then move the clock to the expiration time.

While Microsoft does expose a Fakes library to support testing of static classes I have found the following two approaches force the developer to approach time from a testability standpoint:

Service Class

In the service class model an interface that represents the clock is created and implemented with a production version that returns the current UTC time and a test version that implements a static and controlled instance of the time.

This interface and implementations can then be used with Dependency Injection and Unit test classes. The downside is that service class will need to be consumed by a class that needs the concept of time - which is likely a significant percentage of your services.

    public interface IServiceClock
    {
        DateTimeOffset CurrentTime();
    }

	// Production instance
    public class ServiceClockUtc : IServiceClock
    {
        public DateTimeOffset CurrentTime()
        {
            return DateTimeOffset.UtcNow;
        }
    }
    
    // Testing instance
    public class StaticClock : IServiceClock
    {
        public DateTimeOffset CurrentTime()
        {
            return new DateTimeOffset(
            	new DateTime(2021, 4, 27, 11, 49, 04, DateTimeKind.Utc)
            );
        }
    }

Overridable Static Class

In this approach a new static class is created with an overridable method that is defaulted to the current UTC time. In test classes the  

public static class MyClock
{
    public static Func<DateTimeOffset> CurrentTime = () => {
        return DateTimeOffset.UtcNow;
    }

    public static void ResetToUtc()
    {
        CurrentTime = () => DateTimeOffset.UtcNow;
    }
}

In normal usage this class is used as follows

var currentTime = MyClock.CurrentTime();

In unit tests that requires controlled time the following can be used.

 // SET TIME TO FIXED VALUE FOR A TEST CASE
 MyClock.CurrentTime = () => new DateTimeOffset(new DateTime(2000, 1, 1));

There is some additional care that is required as there is still only one instance of time which may impact concurrent testing but I have found the benefits of continuing to use a static time class outweighs the edge cases in concurrent testing.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.