【问题标题】:Unit testing code that does date processing based on today's date根据今天的日期进行日期处理的单元测试代码
【发布时间】:2009-02-19 13:27:28
【问题描述】:

当代码根据当前日期处理日期时,测试应涵盖边缘情况,例如闰年以及更频繁的月份和年份边界。

在我们的代码中,我们总是使用DateTime.Now(在我们的例子中为.NET)在我们的类中获取当前日期。

如何对这样的代码进行单元测试?

这是Dependency Injection 变得非常有用的地方吗?

编辑

这是一个小问题,但显然下一版本的 Typemock 将允许伪造 DateTime.Now

https://blog.typemock.com/2009/05/mockingfaking-datetimenow-in-unit-tests.html

【问题讨论】:

  • 哇,这个问题的答案很好!

标签: unit-testing datetime language-agnostic dependency-injection


【解决方案1】:

在我们的代码中,我们总是使用 DateTime.Now(在我们的例子中是 .NET)提取当前日期。如何对这样的代码进行单元测试?

这是一个依赖,而且是一个非确定性的依赖。你需要把代码的职责再分担一点。

之前:

  • 有些代码使用当前日期时间来做 X。

之后:

  • 应该有一些代码负责获取当前日期时间。
  • 应该有一些代码使用日期时间来执行 X。

这两组代码不应相互依赖。

这种分离依赖的模式也适用于其他情况(数据库、文件系统等)。

【讨论】:

    【解决方案2】:

    使用 DI 注入“当前日期和时间”无疑是一种矫枉过正。我宁愿重构代码以在任意日期运行。也就是说,我会将function calculateRevenue() 更改为function calculateRevenueOn(datetime date)。如果您需要计算当前日期以外的某些日期,这将更容易测试和使用。

    【讨论】:

    【解决方案3】:

    我使用了 Oren Eini(又名 Ayende Rahien)在他的博客 Dealing with time in tests 中讨论的非常务实的方法。

    有一个这样的静态类:

    public static class SystemTime
    {
        public static Func<DateTime> Now = () => DateTime.Now;
    }
    

    你的代码变成:

    Entity.LastChange = SystemTime.Now();
    

    你的测试会变成:

    SystemTime.Now = () => new DateTime(2000,1,1);
    repository.ResetFailures(failedMsgs); 
    SystemTime.Now = () => new DateTime(2000,1,2);
    var msgs = repository.GetAllReadyMessages(); 
    Assert.AreEqual(2, msgs.Length);
    

    【讨论】:

    • +1 这确实是我最喜欢的解决方案。 DateTime.NowSystemTime.Now 之间的交换可以由测试系统完成,就像您在.Net 基于合同的编程中进行合同的静态交换一样。但是,这两种解决方案都存在问题。它们可能依赖于使用 DateTime.Now 的其他库和插件,然后日期之间会出现不匹配,因此语义不等于在时间更改的系统上运行。
    【解决方案4】:

    依赖注入可以解决这个问题,是的。

    例如,您将创建一个带有方法的 IDateTimeProvider 接口:GetDate()。在您的生产代码类中,您将实现 return DateTime.Now。

    在进行单元测试时,正如 Natrium 建议的那样,您可以将其替换为一个模拟对象,一个返回特定日期进行测试的对象。

    【讨论】:

      【解决方案5】:
      public interface ITimeProvider {
          DateTime Now { get; }
      }
      
      public class SystemTimeProvider : ITimeProvider {
          public virtual DateTime Now { get { return DateTime.Now; } }
      }
      
      public class MockTimeProvider : ITimeProvider {
          public virtual DateTime Now { get; set; }
      }
      
      public class ServiceThatDependsOnTime {
          protected virtual ITimeProvider { get; set; }
          public ServiceThatDependsOnTime(ITimeProvider timeProvider) {
              TimeProvider = timeProvider;
          }
          public virtual void DoSomeTimeDependentOperation() {
              var now = TimeProvider.Now;
              //use 'now' in some complex processing.
          }
      }
      
      [Test]
      public virtual void TestTheService() {
          var time = new MockTimeProvider();
          time.Now = DateTime.Now.AddDays(-500);
          var service = new ServiceThatDependsOnTime(time);
          service.DoSomeTimeDependentOperation();
          //check that the service did it right.
      }
      

      【讨论】:

        【解决方案6】:

        始终将核心日期处理逻辑(根据我的经验,通常很容易为此目的而隔离)放入以日期为参数的单独方法中。

        然后,在您的生产代码中,您可以调用它们并将当前日期作为参数提供给它们,并且仍然可以在单元测试中根据您的需要测试边缘案例。

        顺便说一句,我发现在极端情况下非常密集地测试日期逻辑非常重要。它消除了(可能是灾难性的,参考 Zune)错误,否则您永远不会发现这些错误。除了你提到的那些,夏令时切换也可能有问题。

        【讨论】:

          【解决方案7】:

          你可以制作一个模拟 DateTime.Now 的模拟对象

          这里有一个示例 DateTime.Now 被嘲笑的文章:http://geekswithblogs.net/AzamSharp/archive/2008/04/27/121695.aspx

          【讨论】:

            【解决方案8】:

            你已经给出了答案。您可以围绕 datetime 函数编写一个小型包装类,然后可以将其注入需要获取当前日期的类中。将 DateTime.Now 替换为对包装器对象的调用。

            然后,您可以在测试中注入一个存根或模拟对象,为您提供所需的日期。

            另一种解决方案可能是,根据您的代码的工作方式,只需将日期作为参数传递,而不是埋葬对 datetime.Now 的调用。这使您的代码更具可重用性,因为它可以在当前日期以外的时间运行

            【讨论】:

              【解决方案9】:

              为什么不使用假货?使用 Visual Studio 尽可能简单。只需向 System 添加一个假程序集,将 ShimsContext 添加到您的测试中,

              using (ShimsContext.Create())
              {
              

              并设置 DateTime.Now 以返回特定值:

              System.Fakes.ShimDateTime.NowGet = () => new DateTime(2000, 1, 1);
              

              这种方法可确保对 DateTime.Now 的每次调用都重定向到您的代码,因此您无需注入任何内容即可获取当前时间(这应该非常简单),也不需要全局可写变量。

              【讨论】:

              • 看起来不错,尽管当我问原始问题时这并不存在。 :-)
              • @RichardEv 为了完整起见,我只是在此处添加了它。较新的观众可以检查这个答案。 :)
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多