BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles TimeProvider and ITimer: Writing Unit Tests with Time in .NET 8 Preview 4

TimeProvider and ITimer: Writing Unit Tests with Time in .NET 8 Preview 4

Key Takeaways

  • Handling date and time correctly in .NET can be a challenging task.

  • DateTimeOffset, with stored UTC offset and time zone, provides more accuracy for date and time storage than the DateTime structure.

  • Although you can mock external structures like DateTime and DateTimeOffset in unit tests with .NET Fakes, this feature is only available in the Enterprise version of Visual Studio.

  • In .NET 8 Preview 4, Microsoft introduced TimeProvider and ITimer as universal time abstractions for dependency injections and unit testing.

  • TimeProvider is overloaded with properties and methods, providing extensive functionality for managing time-related operations.

Time plays a critical role in software applications. Tracking time zones and testing time-dependent flows bring challenges to developers. The first part of the article covers the history of .NET Date and Time structures, including existing issues and challenges in time calculation. The second part reviews new .NET 8 Preview 4 abstractions that improve dependency injection and unit testing.

Challenges in utilizing old date and time structures in .NET

DateTime

DateTime has been the main structure for storing date and time in .NET since version 1.1. It has a major drawback - a lack of a time zone. To overcome this problem, the Kind property was added with three possible values: Local, Utc, or Unspecified.

By default, DateTime.Now initializes the local time instance with the Kind property equal to Local.

       var now = DateTime.Now;
       Console.WriteLine("Now: {0}", now);
       Console.WriteLine("Kind: {0}", now.Kind);

Output:

Now: 06/15/2023 11:00:00
Kind: Local

For the conversion to UTC, it is possible to call the DateTime.Now.ToUniversalTime() method, or simply use the DateTime.UtcNow property.

       var now = DateTime.Now;
       var utc1 = now.ToUniversalTime();
       Console.WriteLine("UTC 1: {0}",utc1);
       Console.WriteLine("UTC 1 Kind: {0}", utc1.Kind);
		
       var utc2 = DateTime.UtcNow;
       Console.WriteLine("UTC 2: {0}", utc2);
       Console.WriteLine("UTC 2 Kind: {0}", utc2.Kind);

Output:

UTC 1: 06/15/2023 11:00:00
UTC 1 Kind: Utc
UTC 2: 06/15/2023 11:00:00
UTC 2 Kind: Utc

How does .NET understand the time zone difference during translation from local time to UTC if DateTime does not provide this information?

The ToUniversalTime method takes the time zone from the operating system, an approach that might be problematic. Let’s consider a local DateTime.Now instance that is created in a New York server and then transferred to a London server.

After the conversion to UTC simultaneously on both servers, the results will be different:

      var nowInNewYork = DateTime.Now;
	  var utcInNewYork = nowInNewYork.ToUniversalTime();		
	  Console.WriteLine("UTC in New York: {0}", utcInNewYork);

      // Send locally initiated nowInNewYork to a London server
	  var utcInLondon = nowInNewYork.ToUniversalTime();		
      Console.WriteLine("UTC in London:   {0}", utcInLondon);

Output:

UTC in New York: 06/15/2023 11:00:00
UTC in London:   06/15/2023 07:00:00

This approach led to confusion and frustration for developers; the problems were described in the Microsoft blog along with endless Stackoverflow questions.

On this occasion, Microsoft released the Coding Best Practices Using DateTime in the .NET Framework, shifting all responsibility to the developers:

A developer is responsible for keeping track of time-zone information associated with a DateTime value via some external mechanism. Typically this is accomplished by defining another field or variable that you use to record time-zone information when you store a DateTime value type.

.NET expects time-zone information to be paired with the Kind property during date and time restoration: DateTimeKind.Local with TimeZoneInfo.Local, DateTimeKind.Utc with TimeZoneInfo.Utc, and DateTimeKind.Unspecified for anything else. Custom time zone, as Microsoft recommends, requires to use DateTimeKind.Unspecified:


       var nowInNewYork = DateTime.Now;   
	   Console.WriteLine("New York local time: {0}", nowInNewYork);

      var newYorkTimeZone =
 TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
	  nowInNewYork = DateTime.SpecifyKind(nowInNewYork, DateTimeKind.Unspecified);

       // Now on any server, in New York or London, UTC is correct
       var utc = TimeZoneInfo.ConvertTimeToUtc(nowInNewYork, newYorkTimeZone)
	   Console.WriteLine("UTC: {0}", utc);

Output:

New York local time: 06/15/2023 12:00:00
UTC: 06/15/2023 16:00:00

An alternative solution may be to create and store time only in the UTC format:

var dtInUtc = DateTime.UtcNow;

then convert it every time to a local time on the user’s side:

var dtLocal = dtInUtc.ToLocalTime();
// or
var dtLocal = TimeZoneInfo.ConvertTimeToUtc(dtInUtc, clientTimeZone);

Unfortunately, this requires additional code checks against accidental use of DateTime with a local initialization via DateTime.Now, and also does not exclude such nuances as daylight saving rule changes. I will discuss it in the next section.

DateTimeOffset

As an improvement, the DateTimeOffset structure was introduced in .NET 2. It consists of:

  • structure DateTime
  • property Offset storing time difference relative to UTC.

The problem with servers in different time zones will not affect DateTimeOffset.Now - this is not a panacea for all cases.

Assume there is a need to save an April appointment with a doctor in London. The time zone for London in .NET is TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time"), passing it to DateTimeOffset will result in a correct instance, for example, equal to 2023-04-01 14:00:00 with Offset = +1. Due to daylight saving in Britain, Offset equals +1 at the end of March and equals 0 at the beginning of March. In some countries, there is no daylight saving time. For example, in Nigeria, Offset is always equal to +1.

The daylight saving rules are subject to change, with alternation happening almost every year. It will not be possible to determine the origin of date and time just by looking at the Offset value, whether it was created for Britain or Nigeria with the same value "01:00:00":

 var dt = DateTimeOffset.Now; 
 Console.WriteLine(dt.Offset);

Output:

01:00:00

Thus, for international software, it is better to store the time zone for possible recalculation of Offset according to the new rules; TimeZoneInfo class suits this requirement.

Writing unit tests with time before .NET 8 Preview 4

For unit testing, it is important to be able to mock a method call on an object with a custom implementation. As DateTime and DateTimeOffset do not have interfaces, it is possible to create a custom abstraction and later mock them during tests. For example, the following interface can provide an abstraction for DateTimeOffset:

public interface ISystemClock
{
    DateTimeOffset UtcNow { get; }
}

A similar approach was used internally at Microsoft where the same code was added to at least four different areas in .NET.

What about alternative solutions?

Jon Skeet created the NodaTime library with the correct time processing of non-trivial cases, and of course, with the support of abstractions.

Alright, using some custom interface for your code base is possible. Though how can you create integration tests with external libraries that require time transfer via DateTime and DateTimeOffset types?

Microsoft has a tool called .NET Fakes that can generate mocks and stubs for any .NET library. For example, it is possible to overwrite all static calls of DateTime.Now in unit tests with

System.Fakes.ShimDateTime.NowGet = () => { return new 
DateTime(2025, 12, 31); };

It works, but there are limitations.

First, the generator is compatible only with Windows OS. Second, at the time of writing, it is included only in the Visual Studio Enterprise version, priced at $250 per month. Third, it is a complex and the most advanced integrated development environment tool from Microsoft. It takes a lot of resources and local storage space, compared to the lightweight Visual Studio Code IDE.

Writing unit tests with time in .NET 8 Preview 4

The long-awaited time abstractions were added after years of debates and hundreds of comments for .NET 8 RC: TimeProvider and ITimer.

public abstract class TimeProvider
{
    public static TimeProvider System { get; }
    protected TimeProvider()
    public virtual DateTimeOffset GetUtcNow()
    public DateTimeOffset GetLocalNow()
    public virtual TimeZoneInfo LocalTimeZone { get; }
    public virtual long TimestampFrequency { get; }
    public virtual long GetTimestamp()
    public TimeSpan GetElapsedTime(long startingTimestamp)
    public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
    public virtual ITimer CreateTimer(TimerCallback callback, object? state,TimeSpan dueTime, TimeSpan period)
}

public interface ITimer : IDisposable, IAsyncDisposable
{
    bool Change(TimeSpan dueTime, TimeSpan period);
}


Ultimately, it did not turn out to be flawless, but this represents significant progress.

TimeProvider disadvantages

1. The abstract class came out bulky. For a method that takes a TimeProvider argument, it is not possible to decide which method to mock without knowing internal details: GetUtcNow(), GetLocalNow(), CreateTimer(...), or all of them. Developers proposed to break the new type into small interfaces, in particular, similar to the one already used internally at Microsoft:

public interface ISystemClock
{
    public DateTimeOffset GetUtcNow();
}

public abstract class TimeProvider: ISystemClock {
    // ...
}

That idea had been rejected.

2. An instance of TimeProvider can be created with the help of a static TimeProvider.System call. It is very easy to use for a developer, though it is not so different from the old usage of static DateTime.Now. Later it will result in problems with unit test writing without a special FakeTimeProvider.
Instead of a direct static call, it is expected that developers will use Dependency Injection for TimeProvider. For example, code for ASP.NET Core can look like

public class MyService
{
    public readonly TimeProvider _timeProvider;

    public MyService(TimeProvider timeProvider){
        _timeProvider = timeProvider;
    }

    public boolean IsMonday() {
        return _timeProvider.GetLocalNow().DayOfWeek == DayOfWeek.Monday;   
    }         
}

// Dependency injection:
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<MyService>();

This can be non-trivial for beginner programmers.

TimeProvider and ITimer advantages

1. Time-dependent unit testing becomes more universal. TimeProvider was added to BCL to support a wide variety of .NET runtimes. It becomes possible to cover the above MyService example with a unit test:

using Moq;
using NUnit.Framework;

[Test]
public void MyTest()
{
    var mock = new Mock<TimeProvider>();
 mock.Setup(x => x.GetLocalNow()).Returns(new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.Zero));
    var mockedTimeProvider = mock.Object;

    var myService = new MyService(mockedTimeProvider);
    var result = myService.IsMonday(mockedTimeProvider);
    Assert.IsTrue(result, "Should be Monday");
}

2. The Microsoft team did not bring in the new implementation of the old error where properties had side effects. This is how DateTime.Now property was mistakenly introduced instead of the DateTime.Now() function. TimeProvider abstracts time side effects with functions and methods: GetUtcNow(), GetLocalNow(), GetTimestamp(), etc.

3. It is possible to test time series events with TimeProvider.CreateTimer(...) and Timer.Change(...) functions. This is especially important for Task.Delay(...) and Task.WaitAsync(...)function calls, which now also accept a TimeProvider argument.

4. There is a plan to create FakeTimeProvider as part of .NET to further simplify unit testing. Perhaps then the negative point 2 will not be relevant.

Stephen Toub, a software engineer at Microsoft, wrote:

At the end of the day, we expect almost no one will use anything other than TimeProvider.System in production usage. Unlike many abstractions then, this one is special: it exists purely for testability.

Conclusion

The introduction of the class TimeProvider in .NET 8 Preview 4 specifies a standardized and unified abstraction for managing time. While it may have a few minor drawbacks, Microsoft teams internally marked their custom time interfaces as obsolete and now advocate for adopting TimeProvider.

About the Author

Rate this Article

Adoption
Style

BT