模拟您正在运行单元测试的国家/地区时区

Jim*_*988 7 c# timezone unit-testing

我有一个在“美国东部”Azure 服务器上运行的客户端。在开发中(在英国服务器上)运行良好的一些代码不在该服务器(美国东部服务器)上。我相信这个问题是由于我将日期字符串转换为 UTC 日期时间,但我想为它编写一个测试来证明我已经解决了这个问题。

有没有办法伪造我的单元测试在不同时区运行的事实?

例如,DateTime.Now 应该返回美国东部而不是英国的时间。

这可能吗?

0xc*_*ced 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

该类FakeLocalTimeZone使用ReflectionMagic访问私有字段(受lock 保护),因此不要在生产代码中使用它,仅在单元测试中使用!

以下是如何使用它:

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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这回答了如何伪造我的单元测试在不同时区运行的事实

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

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


小智 6

TimeZoneInfo.ClearCachedData()使用的简化版本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();
    }
}
Run Code Online (Sandbox Code Playgroud)