我有一些单元测试预计“当前时间”与 DateTime.Now 不同,显然我不想更改计算机的时间。
实现这一目标的最佳策略是什么?
我有一些单元测试预计“当前时间”与 DateTime.Now 不同,显然我不想更改计算机的时间。
实现这一目标的最佳策略是什么?
最好的策略是将当前时间包装在一个抽象中,并将该抽象注入到消费者中。
或者,您也可以将时间抽象定义为环境上下文:
public abstract class TimeProvider
{
private static TimeProvider current =
DefaultTimeProvider.Instance;
public static TimeProvider Current
{
get { return TimeProvider.current; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
TimeProvider.current = value;
}
}
public abstract DateTime UtcNow { get; }
public static void ResetToDefault()
{
TimeProvider.current = DefaultTimeProvider.Instance;
}
}
这将使您能够像这样使用它:
var now = TimeProvider.Current.UtcNow;
在单元测试中,您可以替换TimeProvider.Current
为 Test Double/Mock 对象。使用起订量的示例:
var timeMock = new Mock<TimeProvider>();
timeMock.SetupGet(tp => tp.UtcNow).Returns(new DateTime(2010, 3, 11));
TimeProvider.Current = timeMock.Object;
但是,当使用静态状态进行单元测试时,请始终记住通过调用TimeProvider.ResetToDefault()
.
这些都是很好的答案,这是我在另一个项目中所做的:
用法:
获取今天的真实日期时间
var today = SystemTime.Now().Date;
您需要使用SystemTime.Now()
...而不是使用 DateTime.Now,这并不难改变,但此解决方案可能并不适合所有项目。
时间旅行(让我们在未来 5 年)
SystemTime.SetDateTime(today.AddYears(5));
“今天”获取我们的假货(距离“今天”还有 5 年)
var fakeToday = SystemTime.Now().Date;
重置日期
SystemTime.ResetDateTime();
/// <summary>
/// Used for getting DateTime.Now(), time is changeable for unit testing
/// </summary>
public static class SystemTime
{
/// <summary> Normally this is a pass-through to DateTime.Now, but it can be overridden with SetDateTime( .. ) for testing or debugging.
/// </summary>
public static Func<DateTime> Now = () => DateTime.Now;
/// <summary> Set time to return when SystemTime.Now() is called.
/// </summary>
public static void SetDateTime(DateTime dateTimeNow)
{
Now = () => dateTimeNow;
}
/// <summary> Resets SystemTime.Now() to return DateTime.Now.
/// </summary>
public static void ResetDateTime()
{
Now = () => DateTime.Now;
}
}
痣:
[Test]
public void TestOfDateTime()
{
var firstValue = DateTime.Now;
MDateTime.NowGet = () => new DateTime(2000,1,1);
var secondValue = DateTime.Now;
Assert(firstValue > secondValue); // would be false if 'moleing' failed
}
免责声明 - 我在 Moles 上工作
你有一些选择:
使用模拟框架并使用 DateTimeService(实现一个小型包装类并将其注入生产代码)。包装器实现将访问 DateTime 并且在测试中您将能够模拟包装器类。
使用Typemock Isolator,它可以伪造 DateTime.Now并且不需要您更改被测代码。
使用Moles,它也可以伪造 DateTime.Now并且不需要更改生产代码。
一些例子:
使用起订量的包装类:
[Test]
public void TestOfDateTime()
{
var mock = new Mock<IDateTime>();
mock.Setup(fake => fake.Now)
.Returns(new DateTime(2000, 1, 1));
var result = new UnderTest(mock.Object).CalculateSomethingBasedOnDate();
}
public class DateTimeWrapper : IDateTime
{
public DateTime Now { get { return DateTime.Now; } }
}
使用 Isolator 直接伪造 DateTime:
[Test]
public void TestOfDateTime()
{
Isolate.WhenCalled(() => DateTime.Now).WillReturn(new DateTime(2000, 1, 1));
var result = new UnderTest().CalculateSomethingBasedOnDate();
}
免责声明 - 我在 Typemock 工作
为系统添加一个假程序集(右键单击系统参考=>添加假程序集)。
并写入您的测试方法:
using (ShimsContext.Create())
{
System.Fakes.ShimDateTime.NowGet = () => new DateTime(2014, 3, 10);
MethodThatUsesDateTimeNow();
}
关于@crabcrusherclamcollector 的答案,在 EF 查询中使用该方法时会出现问题(System.NotSupportedException:LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”)。我修改了实现:
public static class SystemTime
{
private static Func<DateTime> UtcNowFunc = () => DateTime.UtcNow;
public static void SetDateTime(DateTime dateTimeNow)
{
UtcNowFunc = () => dateTimeNow;
}
public static void ResetDateTime()
{
UtcNowFunc = () => DateTime.UtcNow;
}
public static DateTime UtcNow
{
get
{
DateTime now = UtcNowFunc.Invoke();
return now;
}
}
}
要测试依赖于 的代码,System.DateTime
必须system.dll
模拟 。
Microsoft fakes 需要 Visual Studio 2012 最后通牒,并且直接在康普顿工作。
Smocks 是开源的,非常易于使用。它可以使用 NuGet 下载。
下面显示了一个模拟System.DateTime
:
Smock.Run(context =>
{
context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));
// Outputs "2000"
Console.WriteLine(DateTime.Now.Year);
});
线程安全SystemClock
使用ThreadLocal<T>
对我来说非常有用。
ThreadLocal<T>
在 .Net Framework v4.0 及更高版本中可用。
/// <summary>
/// Provides access to system time while allowing it to be set to a fixed <see cref="DateTime"/> value.
/// </summary>
/// <remarks>
/// This class is thread safe.
/// </remarks>
public static class SystemClock
{
private static readonly ThreadLocal<Func<DateTime>> _getTime =
new ThreadLocal<Func<DateTime>>(() => () => DateTime.Now);
/// <inheritdoc cref="DateTime.Today"/>
public static DateTime Today
{
get { return _getTime.Value().Date; }
}
/// <inheritdoc cref="DateTime.Now"/>
public static DateTime Now
{
get { return _getTime.Value(); }
}
/// <inheritdoc cref="DateTime.UtcNow"/>
public static DateTime UtcNow
{
get { return _getTime.Value().ToUniversalTime(); }
}
/// <summary>
/// Sets a fixed (deterministic) time for the current thread to return by <see cref="SystemClock"/>.
/// </summary>
public static void Set(DateTime time)
{
if (time.Kind != DateTimeKind.Local)
time = time.ToLocalTime();
_getTime.Value = () => time;
}
/// <summary>
/// Resets <see cref="SystemClock"/> to return the current <see cref="DateTime.Now"/>.
/// </summary>
public static void Reset()
{
_getTime.Value = () => DateTime.Now;
}
}
使用示例:
[TestMethod]
public void Today()
{
SystemClock.Set(new DateTime(2015, 4, 3));
DateTime expectedDay = new DateTime(2015, 4, 2);
DateTime yesterday = SystemClock.Today.AddDays(-1D);
Assert.AreEqual(expectedDay, yesterday);
SystemClock.Reset();
}
您可以将您正在测试的类更改为使用Func<DateTime>
将通过它的构造函数参数传递的类,因此当您在实际代码中创建类的实例时,您可以传递() => DateTime.UtcNow
给Func<DateTime>
参数,并且在测试时,您可以传递时间想测试。
例如:
[TestMethod]
public void MyTestMethod()
{
var instance = new MyClass(() => DateTime.MinValue);
Assert.AreEqual(instance.MyMethod(), DateTime.MinValue);
}
public void RealWorldInitialization()
{
new MyClass(() => DateTime.UtcNow);
}
class MyClass
{
private readonly Func<DateTime> _utcTimeNow;
public MyClass(Func<DateTime> UtcTimeNow)
{
_utcTimeNow = UtcTimeNow;
}
public DateTime MyMethod()
{
return _utcTimeNow();
}
}
DateTime.Now
关于使用TypeMock进行模拟的一个特别说明......
的值DateTime.Now
必须放入变量中才能正确模拟。例如:
这不起作用:
if ((DateTime.Now - message.TimeOpened.Value) > new TimeSpan(1, 0, 0))
但是,这样做:
var currentDateTime = DateTime.Now;
if ((currentDateTime - message.TimeOpened.Value) > new TimeSpan(1, 0, 0))
我遇到了同样的问题,但发现微软的一个研究项目解决了这个问题。
http://research.microsoft.com/en-us/projects/moles/
Moles 是一个轻量级的框架,用于 .NET 中基于委托的测试存根和弯路。Moles 可用于绕过任何 .NET 方法,包括密封类型中的非虚拟/静态方法
// Let's detour DateTime.Now
MDateTime.NowGet = () => new DateTime(2000,1, 1);
if (DateTime.Now == new DateTime(2000, 1, 1);
{
throw new Exception("Wahoo we did it!");
}
示例代码是从原始代码修改而来的。
我已经完成了其他建议并将 DateTime 抽象为提供程序。只是感觉不对,我觉得这对于测试来说太多了。今晚我将把它应用到我的个人项目中。
一个老问题,但仍然有效。
我的方法是创建一个新的接口和类来包装System.DateTime.Now
调用
public interface INow
{
DateTime Execute();
}
public sealed class Now : INow
{
public DateTime Execute()
{
return DateTime.Now
}
}
这个接口可以注入到任何需要获取当前日期和时间的类中。在这个例子中,我有一个类将时间跨度添加到当前日期和时间(可单元测试的 System.DateTime.Now.Add(TimeSpan))
public interface IAddTimeSpanToCurrentDateAndTime
{
DateTime Execute(TimeSpan input);
}
public class AddTimeSpanToCurrentDateAndTime : IAddTimeSpanToCurrentDateAndTime
{
private readonly INow _now;
public AddTimeSpanToCurrentDateAndTime(INow now)
{
this._now = now;
}
public DateTime Execute(TimeSpan input)
{
var currentDateAndTime = this._now.Execute();
return currentDateAndTime.Add(input);
}
}
并且可以编写测试以确保其正常运行。我正在使用 NUnit 和 Moq,但任何测试框架都可以
public class Execute
{
private Moq.Mock<INow> _nowMock;
private AddTimeSpanToCurrentDateAndTime _systemUnderTest;
[SetUp]
public void Initialize()
{
this._nowMock = new Moq.Mock<INow>(Moq.MockBehavior.Strict);
this._systemUnderTest = AddTimeSpanToCurrentDateAndTime(
this._nowMock.Object);
}
[Test]
public void AddTimeSpanToCurrentDateAndTimeExecute0001()
{
// arrange
var input = new TimeSpan(911252);
// arrange : mocks
this._nowMock
.Setup(a => a.Execute())
.Returns(new DateTime(348756););
// arrange : expected
var expected = new DateTime(911252 + 348756);
// act
var actual = this._systemUnderTest.Execute(input).Result;
// assert
Assert.Equals(actual, expected);
}
}
这种模式适用于任何依赖于外部因素的函数,比如System.Random.Next()
,System.DateTime.Now.UtcNow
等System.Guid.NewGuid()
。
有关更多示例,请参阅https://loadlimited.visualstudio.com/Stamina/_git/Stamina.Core或获取https://www.nuget.org/packages/Stamina.Core nuget 包。
我们使用的是静态 SystemTime 对象,但在运行并行单元测试时遇到了问题。我尝试使用 Henk van Boeijen 的解决方案,但在生成的异步线程中遇到了问题,最终以类似于下面的方式使用 AsyncLocal:
public static class Clock
{
private static Func<DateTime> _utcNow = () => DateTime.UtcNow;
static AsyncLocal<Func<DateTime>> _override = new AsyncLocal<Func<DateTime>>();
public static DateTime UtcNow => (_override.Value ?? _utcNow)();
public static void Set(Func<DateTime> func)
{
_override.Value = func;
}
public static void Reset()
{
_override.Value = null;
}
}
来自https://gist.github.com/CraftyFella/42f459f7687b0b8b268fc311e6b4af08
我很惊讶没有人提出最明显的方法之一:
public class TimeDependentClass
{
public void TimeDependentMethod(DateTime someTime)
{
if (GetCurrentTime() > someTime) DoSomething();
}
protected virtual DateTime GetCurrentTime()
{
return DateTime.Now; // or UtcNow
}
}
然后你可以在你的测试替身中简单地覆盖这个方法。
在某些情况下,我也有点喜欢注入一个TimeProvider
类,但对于其他人来说,这已经绰绰有余了。TimeProvider
如果你需要在几个类中重用它,我可能会喜欢这个版本。
编辑:对于任何感兴趣的人,这被称为向您的课程添加“接缝”,您可以在这一点上挂钩它的行为来修改它(出于测试目的或其他目的),而无需实际更改类中的代码。
好的做法是,当 DateTimeProvider 实现 IDisposable 时。
public class DateTimeProvider : IDisposable
{
[ThreadStatic]
private static DateTime? _injectedDateTime;
private DateTimeProvider()
{
}
/// <summary>
/// Gets DateTime now.
/// </summary>
/// <value>
/// The DateTime now.
/// </value>
public static DateTime Now
{
get
{
return _injectedDateTime ?? DateTime.Now;
}
}
/// <summary>
/// Injects the actual date time.
/// </summary>
/// <param name="actualDateTime">The actual date time.</param>
public static IDisposable InjectActualDateTime(DateTime actualDateTime)
{
_injectedDateTime = actualDateTime;
return new DateTimeProvider();
}
public void Dispose()
{
_injectedDateTime = null;
}
}
接下来,您可以注入您的假 DateTime 进行单元测试
using (var date = DateTimeProvider.InjectActualDateTime(expectedDateTime))
{
var bankAccount = new BankAccount();
bankAccount.DepositMoney(600);
var lastTransaction = bankAccount.Transactions.Last();
Assert.IsTrue(expectedDateTime.Equals(bankAccount.Transactions[0].TransactionDate));
}
模拟对象。
返回适合您的测试的 Now 的模拟 DateTime。
一种干净的方法是注入 VirtualTime。它可以让你控制时间。首先安装 VirtualTime
Install-Package VirtualTime
例如,这允许在对 DateTime.Now 或 UtcNow 的所有调用中使时间移动快 5 倍
var DateTime = DateTime.Now.ToVirtualTime(5);
让时间变慢,例如慢5倍
var DateTime = DateTime.Now.ToVirtualTime(0.5);
让时间静止
var DateTime = DateTime.Now.ToVirtualTime(0);
时间倒流尚未经过测试
这是一个示例测试:
[TestMethod]
public void it_should_make_time_move_faster()
{
int speedOfTimePerMs = 1000;
int timeToPassMs = 3000;
int expectedElapsedVirtualTime = speedOfTimePerMs * timeToPassMs;
DateTime whenTimeStarts = DateTime.Now;
ITime time = whenTimeStarts.ToVirtualTime(speedOfTimePerMs);
Thread.Sleep(timeToPassMs);
DateTime expectedTime = DateTime.Now.AddMilliseconds(expectedElapsedVirtualTime - timeToPassMs);
DateTime virtualTime = time.Now;
Assert.IsTrue(TestHelper.AreEqualWithinMarginOfError(expectedTime, virtualTime, MarginOfErrorMs));
}
您可以在此处查看更多测试:
DateTime.Now.ToVirtualTime 扩展为您提供的是 ITime 的实例,您将其传递给依赖于 ITime 的方法/类。在您选择的 DI 容器中设置了一些 DateTime.Now.ToVirtualTime
这是另一个注入类委托者的示例
public class AlarmClock
{
private ITime DateTime;
public AlarmClock(ITime dateTime, int numberOfHours)
{
DateTime = dateTime;
SetTime = DateTime.UtcNow.AddHours(numberOfHours);
Task.Run(() =>
{
while (!IsAlarmOn)
{
IsAlarmOn = (SetTime - DateTime.UtcNow).TotalMilliseconds < 0;
}
});
}
public DateTime SetTime { get; set; }
public bool IsAlarmOn { get; set; }
}
[TestMethod]
public void it_can_be_injected_as_a_dependency()
{
//virtual time has to be 1000*3.75 faster to get to an hour
//in 1000 ms real time
var dateTime = DateTime.Now.ToVirtualTime(1000 * 3.75);
var numberOfHoursBeforeAlarmSounds = 1;
var alarmClock = new AlarmClock(dateTime, numberOfHoursBeforeAlarmSounds);
Assert.IsFalse(alarmClock.IsAlarmOn);
System.Threading.Thread.Sleep(1000);
Assert.IsTrue(alarmClock.IsAlarmOn);
}
我遇到了同样的问题,但我想我们不应该在同一个班级使用设置的日期时间。因为它可能导致有一天误用。所以我使用了像这样的提供者
public class DateTimeProvider
{
protected static DateTime? DateTimeNow;
protected static DateTime? DateTimeUtcNow;
public DateTime Now
{
get
{
return DateTimeNow ?? System.DateTime.Now;
}
}
public DateTime UtcNow
{
get
{
return DateTimeUtcNow ?? System.DateTime.UtcNow;
}
}
public static DateTimeProvider DateTime
{
get
{
return new DateTimeProvider();
}
}
protected DateTimeProvider()
{
}
}
对于测试,在测试项目中创建了一个助手来处理设置的东西,
public class MockDateTimeProvider : DateTimeProvider
{
public static void SetNow(DateTime now)
{
DateTimeNow = now;
}
public static void SetUtcNow(DateTime utc)
{
DateTimeUtcNow = utc;
}
public static void RestoreAsDefault()
{
DateTimeNow = null;
DateTimeUtcNow = null;
}
}
关于代码
var dateTimeNow = DateTimeProvider.DateTime.Now //not DateTime.Now
var dateTimeUtcNow = DateTimeProvider.DateTime.UtcNow //not DateTime.UtcNow
并在测试中
[Test]
public void Mocked_Now()
{
DateTime now = DateTime.Now;
MockDateTimeProvider.SetNow(now); //set to mock
Assert.AreEqual(now, DateTimeProvider.DateTime.Now);
Assert.AreNotEqual(now, DateTimeProvider.DateTime.UtcNow);
}
[Test]
public void Mocked_UtcNow()
{
DateTime utcNow = DateTime.UtcNow;
MockDateTimeProvider.SetUtcNow(utcNow); //set to mock
Assert.AreEqual(utcNow, DateTimeProvider.DateTime.UtcNow);
Assert.AreNotEqual(utcNow, DateTimeProvider.DateTime.Now);
}
但是需要记住一件事,有时真实的 DateTime 和提供者的 DateTime 的行为并不相同
[Test]
public void Now()
{
Assert.AreEqual(DateTime.Now.Kind, DateTimeProvider.DateTime.Now.Kind);
Assert.LessOrEqual(DateTime.Now, DateTimeProvider.DateTime.Now);
Assert.LessOrEqual(DateTimeProvider.DateTime.Now - DateTime.Now, TimeSpan.FromMilliseconds(1));
}
我假设尊重将是最大TimeSpan.FromMilliseconds(0.00002)。但大多数时候甚至更少
在MockSamples上查找样品
使用ITimeProvider
我们被迫将其纳入必须从其他项目的其余部分引用的特殊共享公共项目中。但这使依赖项的控制变得复杂。
我们ITimeProvider
在 .NET 框架中搜索。我们搜索了 NuGet 包,发现一个无法使用DateTimeOffset
.
所以我们想出了自己的解决方案,它只取决于标准库的类型。我们正在使用Func<DateTimeOffset>
.
public class ThingThatNeedsTimeProvider
{
private readonly Func<DateTimeOffset> now;
private int nextId;
public ThingThatNeedsTimeProvider(Func<DateTimeOffset> now)
{
this.now = now;
this.nextId = 1;
}
public (int Id, DateTimeOffset CreatedAt) MakeIllustratingTuple()
{
return (nextId++, now());
}
}
自动法
builder.RegisterInstance<Func<DateTimeOffset>>(() => DateTimeOffset.Now);
(对于未来的编辑:在此处附加您的案例)。
public void MakeIllustratingTuple_WhenCalled_FillsCreatedAt()
{
DateTimeOffset expected = CreateRandomDateTimeOffset();
DateTimeOffset StubNow() => expected;
var thing = new ThingThatNeedsTimeProvider(StubNow);
var (_, actual) = thing.MakeIllustratingTuple();
Assert.AreEqual(expected, actual);
}
这是我对这个问题的回答。我将“环境上下文”模式与 IDisposable 结合起来。因此,您可以在正常程序代码中使用 DateTimeProvider.Current,并在测试中使用 using 语句覆盖范围。
using System;
using System.Collections.Immutable;
namespace ambientcontext {
public abstract class DateTimeProvider : IDisposable
{
private static ImmutableStack<DateTimeProvider> stack = ImmutableStack<DateTimeProvider>.Empty.Push(new DefaultDateTimeProvider());
protected DateTimeProvider()
{
if (this.GetType() != typeof(DefaultDateTimeProvider))
stack = stack.Push(this);
}
public static DateTimeProvider Current => stack.Peek();
public abstract DateTime Today { get; }
public abstract DateTime Now {get; }
public void Dispose()
{
if (this.GetType() != typeof(DefaultDateTimeProvider))
stack = stack.Pop();
}
// Not visible Default Implementation
private class DefaultDateTimeProvider : DateTimeProvider {
public override DateTime Today => DateTime.Today;
public override DateTime Now => DateTime.Now;
}
}
}
以下是如何在单元测试中使用上述 DateTimeProvider
using System;
using Xunit;
namespace ambientcontext
{
public class TestDateTimeProvider
{
[Fact]
public void TestDateTime()
{
var actual = DateTimeProvider.Current.Today;
var expected = DateTime.Today;
Assert.Equal<DateTime>(expected, actual);
using (new MyDateTimeProvider(new DateTime(2012,12,21)))
{
Assert.Equal(2012, DateTimeProvider.Current.Today.Year);
using (new MyDateTimeProvider(new DateTime(1984,4,4)))
{
Assert.Equal(1984, DateTimeProvider.Current.Today.Year);
}
Assert.Equal(2012, DateTimeProvider.Current.Today.Year);
}
// Fall-Back to Default DateTimeProvider
Assert.Equal<int>(expected.Year, DateTimeProvider.Current.Today.Year);
}
private class MyDateTimeProvider : DateTimeProvider
{
private readonly DateTime dateTime;
public MyDateTimeProvider(DateTime dateTime):base()
{
this.dateTime = dateTime;
}
public override DateTime Today => this.dateTime.Date;
public override DateTime Now => this.dateTime;
}
}
}
也许不那么专业但更简单的解决方案可能是在消费者方法中创建一个 DateTime 参数。例如,不要像 SampleMethod 那样制作方法,而是使用参数制作 SampleMethod1。SampleMethod1 的测试更容易
public void SampleMethod()
{
DateTime anotherDateTime = DateTime.Today.AddDays(-10);
if ((DateTime.Now-anotherDateTime).TotalDays>10)
{
}
}
public void SampleMethod1(DateTime dateTimeNow)
{
DateTime anotherDateTime = DateTime.Today.AddDays(-10);
if ((dateTimeNow - anotherDateTime).TotalDays > 10)
{
}
}
以下代码适用于我:
bizDeedMock.Verify(p => p.SetDeed(It.Is<DsPostList>(x => x.PostLists[0].registerDate.Year == DateTime.Now.Year)));
bizDeedMock.Verify(p => p.SetDeed(It.Is<DsPostList>(x => x.PostLists[0].registerDate.Month == DateTime.Now.Month)));
bizDeedMock.Verify(p => p.SetDeed(It.Is<DsPostList>(x => x.PostLists[0].registerDate.Day == DateTime.Now.Day)));