我有一个客户端运行在“东部美国”Azure服务器上。在开发中(在英国服务器上)正常工作的代码,在那个服务器上(东部美国服务器)却不行。我认为问题是因为我将日期字符串转换为UTC日期时间,但我想编写一个测试来确实证明我已经解决了这个问题。
有没有一种方法可以伪造我的单元测试正在运行在不同的时区?
例如,DateTime.Now应该返回East US的时间而不是UK的时间。
这可行吗?
我有一个客户端运行在“东部美国”Azure服务器上。在开发中(在英国服务器上)正常工作的代码,在那个服务器上(东部美国服务器)却不行。我认为问题是因为我将日期字符串转换为UTC日期时间,但我想编写一个测试来确实证明我已经解决了这个问题。
有没有一种方法可以伪造我的单元测试正在运行在不同的时区?
例如,DateTime.Now应该返回East US的时间而不是UK的时间。
这可行吗?
是的,您可以伪造单元测试运行的时区。
以下是一个简单的类,它将本地时区更改为构造函数中提供的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具有所有抽象和类型,可以正确处理日期和时间。即使您不打算使用它,您也应该阅读其用户指南,您会学到很多。
_localTimeZone
切换到m_localTimeZone
,即:typeof(TimeZoneInfo).AsDynamicType().s_cachedData.m_localTimeZone = timeZoneInfo;
- Dariusz Woźniak在 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();
}
}
.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);
}
}
<PackageReference Include="ReflectionMagic" Version="4.1.0" />
吗?因为使用ReflectionMagic和手动使用System.Reflection,我的版本和你的版本中TimeZoneInfo私有字段(s_cachedData
和_localTimeZone
)看起来完全相同。 - 0xced使用方法:
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
IDateTimeProvider
,并且添加一个DateTime
类的包装器,例如DateTimeProvider
,它实现了IDateTimeProvider
接口。通过构造函数注入将其传递给所有需要使用DateTime
类的类,并在测试中使用一些MockDateTimeProvider : IDateTimeProvider
。 - user3292642