Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock the country timezone you are running unit test from

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?

like image 730
Jimmyt1988 Avatar asked Jun 07 '17 13:06

Jimmyt1988


Video Answer


2 Answers

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.

like image 59
0xced Avatar answered Sep 21 '22 02:09

0xced


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);
        }
    }

like image 32
Mikael Eliasson Avatar answered Sep 22 '22 02:09

Mikael Eliasson