代码之家  ›  专栏  ›  技术社区  ›  Pedro

单元测试:datetime.now

  •  126
  • Pedro  · 技术社区  · 14 年前

    我有一些单元测试希望“当前时间”与datetime不同。显然,现在我不想更改计算机的时间。

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

    20 回复  |  直到 6 年前
        1
  •  182
  •   heenenee    8 年前

    这个 最好的 策略是 wrap the current time in an abstraction and inject that abstraction into the consumer .


    替代地 也可以将时间抽象定义为 环境语境 :

    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 使用测试双/模拟对象。使用MOQ的示例:

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

    但是,在静态单元测试时,请务必记住 拆下你的固定装置 通过呼叫 TimeProvider.ResetToDefault() .

        2
  •  47
  •   Liam Joshua    9 年前

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

    用途:

    获取今天的实时日期

    var today = SystemTime.Now().Date;
    

    而不是使用日期时间。现在,您需要使用 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;
        }
    }
    
        3
  •  23
  •   Bill the Lizard Alexis MP    12 年前

    鼹鼠:

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

    免责声明-我在研究痣

        4
  •  16
  •   Elisha    11 年前

    您可以选择:

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

    2. 使用 Typemock Isolator 它可以 fake DateTime.Now 并且不会要求您更改测试中的代码。

    3. 使用 Moles 它也可以 fake DateTime.Now 不需要更改生产代码。

    一些例子:

    使用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工作

        5
  •  9
  •   Liam Joshua    9 年前

    为系统添加假程序集(右键单击系统引用=>添加假程序集)。

    写进你的测试方法:

    using (ShimsContext.Create())
    {
       System.Fakes.ShimDateTime.NowGet = () => new DateTime(2014, 3, 10);
       MethodThatUsesDateTimeNow();
    }
    
        6
  •  5
  •   Henk van Boeijen    9 年前

    线程安全 SystemClock 使用 ThreadLocal<T> 对我来说很好。

    线程本地<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();
    }
    
        7
  •  5
  •   marcinn    9 年前

    关于@crabcrusherclamCollector答案,在EF查询中使用该方法时会出现问题(System.NotSupportedException:Linq to实体中不支持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;
                }
            }
        }
    
        8
  •  3
  •   persianLife    8 年前

    测试依赖于 System.DateTime , the system.dll 必须被嘲笑。

    我知道有两个框架可以做到这一点。 Microsoft fakes Smocks .

    微软的假货需要Visual Studio 2012的最后通牒,并且可以直接从康普顿公司工作。

    Smocks是一个开放源代码,非常容易使用。它可以使用nuget下载。

    以下是对 系统时间 :

    Smock.Run(context =>
    {
      context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));
    
       // Outputs "2000"
       Console.WriteLine(DateTime.Now.Year);
    });
    
        9
  •  2
  •   S.Lott    14 年前

    模拟对象。

    一个模拟日期时间,它返回一个适合您的测试的now。

        10
  •  2
  •   Bobby Cannon    11 年前

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

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

    示例代码是从原始代码修改而来的。

    我做了其他人建议的事情,并将日期时间抽象为一个提供者。只是感觉不对,我觉得只是测试太多了。今晚我将把这个应用到我的个人项目中。

        11
  •  2
  •   sara    9 年前

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

    public class TimeDependentClass
    {
        public void TimeDependentMethod(DateTime someTime)
        {
            if (GetCurrentTime() > someTime) DoSomething();
        }
    
        protected virtual DateTime GetCurrentTime()
        {
            return DateTime.Now; // or UtcNow
        }
    }
    

    然后您可以简单地在您的测试双重中重写这个方法。

    我也喜欢注射 TimeProvider 在某些情况下,但对于其他情况,这已经足够了。我可能会赞成 时间提供者 如果您需要在几个类中重用它,那么可以使用版本。

    编辑:对于任何感兴趣的人,这称为向类中添加一个“接缝”,在这个点上,您可以钩住它的行为来修改它(出于测试目的或其他目的),而不必更改类中的代码。

        12
  •  2
  •   smonff David Verdin    6 年前

    一个关于模仿的特别说明 DateTime.Now 具有 TypeMock

    价值 日期时间 必须放入变量中才能正确模拟。例如:

    这不起作用:

    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))
    
        13
  •  1
  •   Mino    8 年前

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

    接下来,您可以为单元测试注入假日期时间。

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

    见例子 Example of DateTimeProvider

        14
  •  0
  •   Dipon Roy    9 年前

    我也遇到了同样的问题,但我想我们不应该在同一个类上使用set datetime。因为有一天它会导致误用。所以我使用了像

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

    但需要记住一件事,有时实际的日期时间和提供程序的日期时间不一样

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

    我以为尊重是最大限度的 时间跨度.从毫秒(0.00002) . 但大多数时候甚至更少

    在以下位置查找示例 MockSamples

        15
  •  0
  •   amcc    8 年前

    未提及的另一种选择是将当前时间注入依赖方法:

    public class DateTimeNowDependencyClass
    {
        ...
    
        public void ImplicitTimeDependencyMethod(Obj arg0)
        {
            this.TimeDependencyMethod(DateTime.Now, arg0);
        }
    
        internal void TimeDependencyMethod(DateTime now, Obj arg1)
        {
            ...
        }
    
        ...
    }
    

    内部变化对单元测试开放,无论是否并行。

    这是基于以下原则: ImplicitTimeDependencyMethod 是“太简单了不能打破”(见: http://junit.sourceforge.net/doc/faq/faq.htm#best_3 )所以不需要包括在单元测试覆盖中。但无论如何,在集成测试中应该接触到它。

    根据类的用途,可能需要将这两个方法都公开。

        16
  •  0
  •   Stefc    7 年前

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

    使用 ITimeProvider 我们被迫接受 特殊共享公共 必须从其他项目中引用的项目。但这 complicated the control of dependencies .

    我们搜索了 iTIME提供者 在.NET框架中。我们搜索了Nuget包,找到了 one 这不适用于 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);
    }
    
        18
  •  0
  •   Mehmet    7 年前

    也许不太专业但更简单的解决方案可以在consumer方法中生成一个datetime参数。例如,不要像sampleMethod这样的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)
            {
    
            }
    
        }
    
        19
  •  0
  •   Samuel    6 年前

    一个干净的方法是注入virtualtime。它允许你控制时间。 首次安装virtualtime

    Install-Package VirtualTime
    

    例如,它允许在所有对datetime的调用中使时间加快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));
    }
    

    您可以在此处查看更多测试:

    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);
    }
    
        20
  •  0
  •   rrrr-o    6 年前

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