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

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


当前回答

您可以更改您正在测试的类使用Func<DateTime>,它将通过它的构造函数参数传递,因此当您在实际代码中创建类的实例时,您可以pass () => 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();
        }
    }

其他回答

最好的策略是将当前时间封装在一个抽象中,并将该抽象注入到使用者中。


或者,你也可以将时间抽象定义为环境上下文:

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。

摩尔数:

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

免责声明-我工作的鼹鼠

测试依赖于系统的代码。DateTime,系统.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);
});

一种简单的方法是注入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);
}

以下是我对这个问题的回答。我将“环境上下文”模式与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;
        }
    }
}