13

I have a client running on an "East US" Azure server. Some of the code that works okay in development (on UK server) does not on that server (East US server). I believe the issue is due to me converting a date string into a UTC date time but i would like to write a test for it to indeed prove i have solved the issue.

Is there a way to fake the fact my unit test is running in a different time zone?

For example, DateTime.Now should return the time in East US rather then UK.

Is this possible?

Dariusz Woźniak
  • 9,640
  • 6
  • 60
  • 73
Jimmyt1988
  • 20,466
  • 41
  • 133
  • 233
  • 4
    We had a similar problem. In short our solution was to introduce an Interface e.g. `IDateTimeProvider` and a wrapper for the `DateTime` class e.g. `DateTimeProvider` which implements `IDateTimeProvider`. Inject through Constructor Injection into all classes which need the `DateTime` class and use some `MockDateTimeProvider : IDateTimeProvider` for your tests. – user3292642 Jun 07 '17 at 13:07
  • 2
    You can use shims - https://learn.microsoft.com/en-us/visualstudio/test/using-shims-to-isolate-your-application-from-other-assemblies-for-unit-testing – Ondrej Svejdar Jun 07 '17 at 13:09
  • @OndrejSvejdar - Ah, now that... is exactly... what i wanted! – Jimmyt1988 Jun 07 '17 at 13:12

4 Answers4

17

Yes, you can fake the timezone in which your unit tests are running.

Here's a simple class that changes the local timezone to the timeZoneInfo provided in the constructor and resets the original local timezone when disposed.

using System;
using ReflectionMagic;

namespace TestProject
{
    public class FakeLocalTimeZone : IDisposable
    {
        private readonly TimeZoneInfo _actualLocalTimeZoneInfo;

        private static void SetLocalTimeZone(TimeZoneInfo timeZoneInfo)
        {
            typeof(TimeZoneInfo).AsDynamicType().s_cachedData._localTimeZone = timeZoneInfo;
        }

        public FakeLocalTimeZone(TimeZoneInfo timeZoneInfo)
        {
            _actualLocalTimeZoneInfo = TimeZoneInfo.Local;
            SetLocalTimeZone(timeZoneInfo);
        }

        public void Dispose()
        {
            SetLocalTimeZone(_actualLocalTimeZoneInfo);
        }
    }
}

The FakeLocalTimeZone class is using ReflectionMagic to access private fields (which are protected by a lock), so don't use this in production code, only in your unit tests!

Here is how you can use it:

using System;
using Xunit;

namespace TestProject
{
    public class UnitTest
    {
        [Fact]
        public void TestFakeLocalTimeZone()
        {
            using (new FakeLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("US/Eastern")))
            {
                // In this scope, the local time zone is US/Eastern
                // Here, DateTime.Now returns 2020-09-02T02:58:46
                Assert.Equal("US/Eastern", TimeZoneInfo.Local.Id);
                Assert.Equal(TimeSpan.FromHours(-5), TimeZoneInfo.Local.BaseUtcOffset);
            }
            // In this scope (i.e. after the FakeLocalTimeZone is disposed) the local time zone is the one of the computer.
            // It is not safe to assume anything about which is the local time zone here.
            // Here, DateTime.Now returns 2020-09-02T08:58:46 (my computer is in the Europe/Zurich time zone)
        }
    }
}

This answers how to fake the fact my unit test is running in a different time zone.

Now, as user3292642 suggested in the comments, a better design would be to use an interface and not call DateTime.Now directly in your code so that you can provide a fake now in your unit tests.

And an even better choice would be to use Noda Time instead of the DateTime type. Noda Time has all the abstractions and types to properly work with date and time. Even if you don't plan to use it, you should read its user guide, you will learn a lot.

0xced
  • 25,219
  • 10
  • 103
  • 255
  • Nice solution and description! For my .NET Framework 4.7.2, I needed switch from `_localTimeZone ` to `m_localTimeZone`, i.e.: `typeof(TimeZoneInfo).AsDynamicType().s_cachedData.m_localTimeZone = timeZoneInfo;` – Dariusz Woźniak May 19 '22 at 07:55
5

Simplified version using TimeZoneInfo.ClearCachedData() in Dispose():

public class LocalTimeZoneInfoMocker : IDisposable
{
    public LocalTimeZoneInfoMocker(TimeZoneInfo mockTimeZoneInfo)
    {
        var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static);
        var cachedData = info.GetValue(null);
        var field = cachedData.GetType().GetField("_localTimeZone",
            BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance);
        field.SetValue(cachedData, mockTimeZoneInfo);
    }

    public void Dispose()
    {
        TimeZoneInfo.ClearCachedData();
    }
}
3

On our version .netcoreapp 3.1 the answer from 0xced didn't work because the internal api looks like it changed slightly. I made some small adjustments to make it work there.

    public class FakeLocalTimeZone : IDisposable
    {
        private readonly TimeZoneInfo _actualLocalTimeZoneInfo;

        private static void SetLocalTimeZone(TimeZoneInfo timeZoneInfo)
        {
            var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static);
            object cachedData = info.GetValue(null);

            var field = cachedData.GetType().GetField("_localTimeZone", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance);
            field.SetValue(cachedData, timeZoneInfo);
        }

        public FakeLocalTimeZone(TimeZoneInfo timeZoneInfo)
        {
            _actualLocalTimeZoneInfo = TimeZoneInfo.Local;
            SetLocalTimeZone(timeZoneInfo);
        }

        public void Dispose()
        {
            SetLocalTimeZone(_actualLocalTimeZoneInfo);
        }
    }

Mikael Eliasson
  • 5,157
  • 23
  • 27
  • Have you added `` in your unit test csproj file? Because the TimeZoneInfo private fields (`s_cachedData` and `_localTimeZone`) look absolutely identical in my version (using ReflectionMagic) and yours (using System.Reflection manually). – 0xced Jan 19 '22 at 07:16
2

You can use the small NuGet library (authored by me) based on both answers. It includes both .NET Framework and .NET Core (including .NET) solutions. It's available here:

Usage:

using (new FakeLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("UTC+12")))
{
    var localDateTime = new DateTime(2020, 12, 31, 23, 59, 59, DateTimeKind.Local);
    var utcDateTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime);

    Assert.That(TimeZoneInfo.Local.Id, Is.EqualTo("UTC+12")); // ✅ Assertion passes
    Assert.That(localDateTime, Is.EqualTo(utcDateTime.AddHours(12))); // ✅ Assertion passes
}

// Now, TimeZoneInfo.Local is the one before setup
Dariusz Woźniak
  • 9,640
  • 6
  • 60
  • 73