仿真运行单元测试的国家时区

14

我有一个客户端运行在“东部美国”Azure服务器上。在开发中(在英国服务器上)正常工作的代码,在那个服务器上(东部美国服务器)却不行。我认为问题是因为我将日期字符串转换为UTC日期时间,但我想编写一个测试来确实证明我已经解决了这个问题。

有没有一种方法可以伪造我的单元测试正在运行在不同的时区?

例如,DateTime.Now应该返回East US的时间而不是UK的时间。

这可行吗?


4
我们曾经遇到过类似的问题。简而言之,我们的解决方案是引入一个接口,例如 IDateTimeProvider,并且添加一个 DateTime 类的包装器,例如 DateTimeProvider,它实现了 IDateTimeProvider 接口。通过构造函数注入将其传递给所有需要使用 DateTime 类的类,并在测试中使用一些 MockDateTimeProvider : IDateTimeProvider - user3292642
2
您可以使用Shims - https://learn.microsoft.com/en-us/visualstudio/test/using-shims-to-isolate-your-application-from-other-assemblies-for-unit-testing - Ondrej Svejdar
@OndrejSvejdar - 啊,现在这就是...我想要的! - Jimmyt1988
4个回答

18

是的,您可以伪造单元测试运行的时区。

以下是一个简单的类,它将本地时区更改为构造函数中提供的timeZoneInfo并在销毁时将原始本地时区重置。

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

FakeLocalTimeZone类使用ReflectionMagic来访问私有字段(这些字段受锁保护),因此不要在生产代码中使用它,只在单元测试中使用。

以下是如何使用它的示例:

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

这里回答了如何伪造单元测试运行在不同的时区的问题。

现在,正如用户3292642在评论中建议的那样,一个更好的设计是使用接口而不是直接在代码中调用DateTime.Now,以便在单元测试中提供一个假的“现在”。

而更好的选择是使用Noda Time而不是DateTime类型。Noda Time具有所有抽象和类型,可以正确处理日期和时间。即使您不打算使用它,您也应该阅读其用户指南,您会学到很多。


很好的解决方案和描述!对于我的.NET Framework 4.7.2,我需要从_localTimeZone切换到m_localTimeZone,即:typeof(TimeZoneInfo).AsDynamicType().s_cachedData.m_localTimeZone = timeZoneInfo; - Dariusz Woźniak

5

Dispose() 中使用 TimeZoneInfo.ClearCachedData() 来简化代码:

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

谢谢您发布这篇文章!我之前在.NET 6上尝试了其他答案,但是都无法正常工作,不过这个方法对我很有用。 - Kenny Drobnack

3
在我们的版本.netcoreapp 3.1上,0xced的答案不起作用,因为内部api似乎轻微更改了。我做了一些小调整使其在那里起作用。
    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);
        }
    }


你在单元测试的csproj文件中添加了<PackageReference Include="ReflectionMagic" Version="4.1.0" />吗?因为使用ReflectionMagic和手动使用System.Reflection,我的版本和你的版本中TimeZoneInfo私有字段(s_cachedData_localTimeZone)看起来完全相同。 - 0xced

2
您可以使用由我编写的小型NuGet库(基于两个答案)。 它包括.NET Framework和.NET Core(包括.NET)解决方案。 该库可在以下位置获取:

使用方法:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接