我有一些单元测试,期望“当前时间”与DateTime不同。显然,我不想改变电脑的时间。
实现这一目标的最佳策略是什么?
我有一些单元测试,期望“当前时间”与DateTime不同。显然,我不想改变电脑的时间。
实现这一目标的最佳策略是什么?
当前回答
使用ThreadLocal<T>的线程安全SystemClock非常适合我。
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();
}
其他回答
好的做法是,当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示例
最好的策略是将当前时间封装在一个抽象中,并将该抽象注入到使用者中。
或者,你也可以将时间抽象定义为环境上下文:
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。当前带有Test Double/Mock对象。使用Moq的例子:
var timeMock = new Mock<TimeProvider>();
timeMock.SetupGet(tp => tp.UtcNow).Returns(new DateTime(2010, 3, 11));
TimeProvider.Current = timeMock.Object;
但是,当使用静态状态进行单元测试时,始终记得通过调用TimeProvider.ResetToDefault()来删除fixture。
一个老问题,但仍然有效。
我的方法是创建一个新的接口和类来包装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 . guide . newguid()等。
参见https://loadlimited.visualstudio.com/Stamina/_git/Stamina.Core获取更多示例或获取https://www.nuget.org/packages/Stamina.Core nuget包。
在使用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());
}
}
如何注册
自发法克
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);
}
下面的代码为我工作:
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)));