单元测试:日期时间。现在

我有一些单元测试,期望“当前时间”与DateTime不同。显然,我不想改变电脑的时间。

实现这一目标的最佳策略是什么?

176273 次浏览

模拟对象。

一个模拟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;

在单元测试中,你可以用test Double/Mock对象替换TimeProvider.Current。使用Moq的例子:

var timeMock = new Mock<TimeProvider>();
timeMock.SetupGet(tp => tp.UtcNow).Returns(new DateTime(2010, 3, 11));
TimeProvider.Current = timeMock.Object;

然而,当使用静态状态进行单元测试时,始终记住通过调用TimeProvider.ResetToDefault()拆掉你的固定装置

你有一些选择:

  1. 使用模拟框架并使用DateTimeService(实现一个小型包装器类并将其注入到生产代码中)。包装器实现将访问DateTime,在测试中您将能够模拟包装器类。

  2. 使用Typemock Isolator,它可以fake DateTime。现在并且不需要您更改测试中的代码。

  3. 使用 mol ,也可以fake DateTime。现在并且不需要更改生产代码。

一些例子:

使用Moq的包装器类:

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

直接使用隔离器伪造日期时间:

[Test]
public void TestOfDateTime()
{
Isolate.WhenCalled(() => DateTime.Now).WillReturn(new DateTime(2000, 1, 1));


var result = new UnderTest().CalculateSomethingBasedOnDate();
}

免责声明-我在Typemock工作

摩尔数:

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

免责声明-我工作的鼹鼠

关于用TypeMock模拟DateTime.Now的一个特别注意…

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

这些都是很好的答案,这是我在另一个项目中所做的:

< em >用法:< / em >

获取今天的真实日期时间

var today = SystemTime.Now().Date;

而不是使用DateTime。现在,你需要使用SystemTime.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;
}
}

我也遇到过同样的问题,但我发现微软的一个研究项目解决了这个问题。

http://research.microsoft.com/en-us/projects/moles/

mole是一个基于委托的。net测试存根和弯路的轻量级框架。mole可以用来绕过任何.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 reference=>添加假程序集)。

并在测试方法中写入:

using (ShimsContext.Create())
{
System.Fakes.ShimDateTime.NowGet = () => new DateTime(2014, 3, 10);
MethodThatUsesDateTimeNow();
}

线程安全的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();
}

我也有同样的问题,但我在想我们不应该在同一类上使用设置日期时间的东西。因为有一天可能会导致滥用。我使用了提供商

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找到样本

关于@crabcrusherclamcollector的回答,在EF查询中使用这种方法时存在问题。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;
}
}
}

我很惊讶没有人提出一个最明显的方法:

public class TimeDependentClass
{
public void TimeDependentMethod(DateTime someTime)
{
if (GetCurrentTime() > someTime) DoSomething();
}


protected virtual DateTime GetCurrentTime()
{
return DateTime.Now; // or UtcNow
}
}

然后,您可以简单地在测试double中重写此方法。

在某些情况下,我也有点喜欢注入TimeProvider类,但对于其他情况,这已经足够了。不过,如果你需要在几个类中重用它,我可能更喜欢TimeProvider版本。

编辑:对于任何感兴趣的人来说,这被称为向类中添加“接缝”,在这个点上,您可以钩入它的行为来修改它(用于测试目的或其他),而无需实际更改类中的代码。

要测试依赖于System.DateTime的代码,必须模拟system.dll

我知道有两个框架可以做到这一点。微软假货罩衫

微软的假货需要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);
});

好的做法是,当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));
}

参见示例DateTimeProvider实例

以下是我对这个问题的回答。我将“环境上下文”模式与IDisposable结合起来。你可以使用DateTimeProvider。当前在您的正常程序代码和测试中,您使用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;
}
}
}

使用ITimeProvider时,我们被迫将它带入特殊共享项目中,必须从其他项目中引用它。但是这个使依赖关系的控制复杂化

我们在. net框架中搜索ITimeProvider。我们搜索了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());
}
}

如何注册

Autofac

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

也许不太专业,但更简单的解决方案可以在消费者方法中创建一个DateTime参数。例如,不使用像SampleMethod这样的make方法,而是使用带参数的make 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)
{


}


}
一个干净的方法是注入VirtualTime。它可以让你控制时间。 首先安装VirtualTime

Install-Package VirtualTime

例如,这允许在所有对DateTime的调用上使时间加快5倍。Now或UtcNow

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

你可以在这里查看更多测试:

https://github.com/VirtualTime/VirtualTime/blob/master/VirtualTimeLib.Tests/when_virtual_time_is_used.cs

什么DateTime.Now.ToVirtualTime扩展给你是一个ITime的实例,你传递给一个依赖于ITime的方法/类。一些DateTime.Now.ToVirtualTime被设置在你选择的DI容器中

下面是注入类构造函数的另一个例子

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

我们使用的是静态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

一个老问题,但仍然有效。

我的方法是创建一个新的接口和类来包装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.UtcNowSystem.Guid.NewGuid()等。

参见https://loadlimited.visualstudio.com/Stamina/_git/Stamina.Core获取更多示例或获取https://www.nuget.org/packages/Stamina.Core nuget包。

你可以将你测试的类更改为使用Func<DateTime>,这个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();
}
}

下面的代码为我工作:

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