什么是在测试期间覆盖DateTime.Now的好方法?


116

我有一些(C#)代码依赖于今天的日期来正确地计算将来的情况。如果我在测试中使用今天的日期,则必须在测试中重复计算,感觉不对。在测试中将日期设置为已知值的最佳方法是什么,以便我可以测试结果是否为已知值?

Answers:


157

我的首选是让使用时间的类实际上依赖于接口,例如

interface IClock
{
    DateTime Now { get; } 
}

具体实施

class SystemClock: IClock
{
     DateTime Now { get { return DateTime.Now; } }
}

然后,如果需要,您可以提供任何其他想要测试的时钟,例如

class StaticClock: IClock
{
     DateTime Now { get { return new DateTime(2008, 09, 3, 9, 6, 13); } }
}

向依赖它的类提供时钟可能会有一些开销,但是可以通过任何数量的依赖项注入解决方案来处理(使用控制容器反转,普通的旧构造函数/ setter注入,甚至是静态网关模式))。

交付提供所需时间的对象或方法的其他机制也可以使用,但是我认为关键是要避免重置系统时钟,因为这只会给其他层次带来麻烦。

此外,DateTime.Now在计算中使用并包括它并不只是感觉不对,还会使您失去测试特定时间的能力,例如,如果您发现仅在午夜或周二附近发生的错误。使用当前时间将无法测试这些情况。或至少不在您想要的时候。


2
实际上,我们在xUnit.net扩展之一中对此进行了形式化。我们有一个Clock类,您可以将其用作静态而不是DateTime,并且可以“冻结”和“解冻”时钟,包括指定的日期。参见is.gd/3xdsis.gd/3xdu
Brad Wilson

2
还值得注意的是,当您要替换系统时钟方法时(例如,当在分支机构分布在广泛的时区的企业中使用全局时钟时,这种情况就会发生),该方法为您提供了宝贵的业务级自由来更改现在的意思。
Mike Burton

1
这种方式对我来说非常有效,并且可以使用依赖注入框架来访问IClock实例。
威尔卡

8
好答案。只是想补充一点,几乎在所有情况下UtcNow都应使用该代码,然后根据代码的关注程度进行适当调整,例如业务逻辑,UI等。跨时区的DateTime操作是一个雷区,但最好的第一步是始终从UTC时间。
亚当·拉尔夫

@BradWilson这些链接现在已断开。也无法在WayBack上将它们拉起。

55

Ayende Rahien 使用一种非常简单的静态方法...

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

1
将存根/模拟点设置为公共全局变量(类静态变量)似乎很危险。将其范围限定在被测系统上不是更好的方法,例如,使其成为被测类的私有静态成员吗​​?
亚伦

1
这是风格问题。这是获得单位测试可更改的系统时间所要做的最少的事情。
Anthony Mastrean 2010年

3
恕我直言,最好使用接口而不是全局静态单例。请考虑以下情形:测试运行程序高效并且可以并行运行尽可能多的测试,一个测试将给定时间更改为X,将另一个更改为Y。我们现在有冲突,并且这两个测试将切换失败。如果我们使用接口,则每个测试都会根据其需求模拟接口,每个测试现在都与其他测试隔离。HTH。
ShloEmi 2015年

17

我认为为诸如获取当前日期之类的简单事件创建单独的时钟类有点过头了。

您可以将今天的日期作为参数传递,以便可以在测试中输入其他日期。这具有使代码更灵活的附加好处。


我对您和布莱尔的答案都+1了,即使他们都反对。我认为这两种方法都是有效的。您的方法我可能会使用一个不使用Unity之类的项目。
RichardOD

1
是的,可以为“现在”添加参数。但是,在某些情况下,这需要您公开通常不希望公开的参数。举例来说,假设您有一个计算日期与现在之间的天数的方法,那么您不想将“ now”作为参数公开,因为这样可以操纵结果。如果是您自己的代码,请继续添加“ now”作为参数,但是如果您在团队中工作,您将永远不知道其他开发人员会将代码用于什么,因此,如果“ now”是代码的关键部分,则需要保护它免受操纵或滥用。
Stitch10925'4

17

使用Microsoft Fakes创建垫片是一种非常简单的方法。假设我有以下课程:

public class MyClass
{
    public string WhatsTheTime()
    {
        return DateTime.Now.ToString();
    }

}

在Visual Studio 2012中,可以通过右键单击要为其创建Fakes / Shims的程序集并选择“添加Fakes程序集”,将Fakes程序集添加到测试项目中。

添加伪装程序集

最后,这是测试类的样子:

using System;
using ConsoleApplication11;
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DateTimeTest
{
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestWhatsTheTime()
    {

        using(ShimsContext.Create()){

            //Arrange
            System.Fakes.ShimDateTime.NowGet =
            () =>
            { return new DateTime(2010, 1, 1); };

            var myClass = new MyClass();

            //Act
            var timeString = myClass.WhatsTheTime();

            //Assert
            Assert.AreEqual("1/1/2010 12:00:00 AM",timeString);

        }
    }
}
}

1
这正是我想要的。谢谢!顺便说一句,在VS 2013
Douglas Ludlow

或如今,VS 2015企业版。对于这样的最佳实践,这是一个遗憾。
RJB

12

成功进行单元测试的关键是去耦。您必须将您感兴趣的代码与其外部依赖项分开,以便可以对其进行单独测试。(幸运的是,测试驱动开发会产生解耦的代码。)

在这种情况下,您的外部设备是当前的DateTime。

我的建议是将处理DateTime的逻辑提取到新方法或类或您认为合适的任何东西,然后传递DateTime。现在,您的单元测试可以传递任意DateTime,以产生可预测的结果。


10

另一个使用Microsoft Moles(.NET隔离框架)。

MDateTime.NowGet = () => new DateTime(2000, 1, 1);

Moles允许用委托替换任何.NET方法。Moles支持静态或非虚拟方法。Moles依赖于Pex的探查器。


这很漂亮,但是需要Visual Studio 2010!:-(
Pandincus 2010年

在VS 2008中也可以正常工作。不过,它最适合与MSTest一起使用。您可以使用NUnit,但是我认为您必须使用特殊的测试运行器来运行测试。
Torbjørn

如果可能,我会避免使用Moles(又名Microsoft Fakes)。理想情况下,它仅应用于无法通过依赖关系注入进行测试的旧代码。
brianpeiris

1
@brianpeiris,使用Microsoft Fakes的缺点是什么?
Ray Cheng

您不能总是DI第三方的内容,因此,如果您的代码调用了不想为单元测试实例化的第三方API,则Fakes完全可以接受单元测试。我同意避免为自己的代码使用假货/摩尔,但对于其他目的,它完全可以接受。
ChrisCW



2

您可以注入用于测试的类的类(更好:method / delegateDateTime.Now。具有DateTime.Now默认值,并且仅在测试中将其设置为返回恒定值的伪方法。

编辑:布莱尔·康拉德(Blair Conrad)说了什么(他有一些代码要看)。除此之外,我倾向于使用委托,因为它们不会像IClock... 那样使您的类层次结构混乱。


1

我经常遇到这种情况,以至于我创建了一个简单的nuget,它通过接口公开了Now属性。

public interface IDateTimeTools
{
    DateTime Now { get; }
}

实施当然非常简单

public class DateTimeTools : IDateTimeTools
{
    public DateTime Now => DateTime.Now;
}

因此,在将nuget添加到我的项目中之后,我可以在单元测试中使用它

在此处输入图片说明

您可以直接从GUI Nuget软件包管理器或使用以下命令安装模块:

Install-Package -Id DateTimePT -ProjectName Project

Nuget的代码在这里

可在此处找到有关Autofac的用法示例。


-11

您是否考虑过使用条件编译来控制调试/部署期间发生的情况?

例如

DateTime date;
#if DEBUG
  date = new DateTime(2008, 09, 04);
#else
  date = DateTime.Now;
#endif

否则,您想公开该属性以便可以对其进行操作,这是编写可测试代码的全部挑战,而这正是我目前正在努力解决的问题:D

编辑

我很大一部分会喜欢布莱尔的方法。这使您可以“热插拔”部分代码以帮助测试。一切都遵循设计原理封装各种测试代码与生产代码没有什么不同,只是没有人从外部看到过它。

尽管对于本示例来说,创建和接口似乎需要做很多工作(这就是为什么我选择条件编译的原因)。


哇,你在这个答案上受了重击 这就是我一直在做的,虽然在一个地方,在那里我称之为替代方法DateTimeNowToday
戴维高柏灵参赞
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.