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

What's a good way to overwrite DateTime.Now during testing?

提问人:Craig.Nicol 提问时间:9/4/2008 最后编辑:Alex BCraig.Nicol 更新时间:6/22/2023 访问量:49516

问:

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

C# 单元 日期时间 测试

评论


答:

18赞 Mendelt 9/4/2008 #1

我认为为获取当前日期等简单的事情创建一个单独的时钟类有点矫枉过正。

您可以将今天的日期作为参数传递,以便在测试中输入不同的日期。这还有一个额外的好处,就是使你的代码更加灵活。

评论

0赞 RichardOD 11/27/2009
我对你和布莱尔的回答都+1,尽管他们都反对。我认为这两种方法都是有效的。你的方法我可能会使用一个不使用 Unity 之类的项目。
1赞 Stitch10925 4/6/2017
是的,可以为“now”添加参数。但是,在某些情况下,这需要您公开通常不想公开的参数。例如,假设您有一个计算日期和现在之间的天数的方法,那么您不希望将“现在”作为参数公开,因为这允许操作您的结果。如果是你自己的代码,请继续添加“now”作为参数,但如果你在一个团队中工作,你永远不知道其他开发人员会使用你的代码做什么,所以如果“现在”是你代码的关键部分,你需要保护它不纵或滥用。
-11赞 Rob Cooper 9/4/2008 #2

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

例如:

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

如果做不到这一点,你就想公开属性,这样你就可以操作它,这都是编写可测试代码的挑战的一部分,这是我目前正在努力解决的问题:D

编辑

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

不过,对于这个例子来说,创建和接口似乎需要做很多工作(这就是我选择条件编译的原因)。

评论

0赞 Dave Cousineau 10/23/2015
哇,你在这个答案上受到了沉重的打击。这就是我一直在做的事情,尽管在一个地方,我调用替换 's 等的方法。DateTimeNowToday
166赞 Blair Conrad 9/4/2008 #3

我的偏好是让使用时间的类实际上依赖于一个接口,例如

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赞 Brad Wilson 10/5/2008
我们实际上在其中一个 xUnit.net 扩展中正式化了这一点。我们有一个 Clock 类,您可以将其用作静态而不是 DateTime,您可以“冻结”和“解冻”时钟,包括特定日期。请参阅 is.gd/3xdsis.gd/3xdu
2赞 Mike Burton 11/8/2008
还值得注意的是,当您想要替换系统时钟方法时(例如,在分支机构分布在广泛分散的时区的企业中使用全局时钟时,就会发生这种情况),此方法为您提供了宝贵的业务级自由来更改“现在”的含义。
1赞 Wilka 11/8/2008
这种方式对我来说非常有效,同时使用依赖注入框架来访问 IClock 实例。
8赞 Adam Ralph 11/15/2013
很好的答案。只是想补充一点,在几乎所有情况下都应该使用,然后根据代码的关注点进行适当的调整,例如业务逻辑、UI 等。跨时区的 DateTime 操作是一个雷区,但最好的第一步是始终以 UTC 时间开始。UtcNow
0赞 5/31/2017
@BradWilson 这些链接现在已断开。也无法在 WayBack 上拉起它们。
3赞 Daren Thomas 9/4/2008 #4

您可以在正在测试的类中注入您使用的类(更好:方法/委托)。必须为默认值,并且仅在测试中将其设置为返回常量值的虚拟方法。DateTime.NowDateTime.Now

编辑:布莱尔·康拉德(Blair Conrad)说了什么(他有一些代码要看)。除了,我倾向于更喜欢代表,因为他们不会用类似的东西弄乱你的班级层次结构......IClock

58赞 Anthony Mastrean 9/4/2008 #5

Ayende Rahien 使用一种相当简单的静态方法......

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

评论

1赞 Aaron 9/19/2008
将存根/模拟点设为公共全局变量(类静态变量)似乎很危险。将它的作用域限定为被测系统不是更好吗 - 例如,使其成为被测类的私有静态成员?
1赞 Anthony Mastrean 1/31/2010
这是一个风格问题。这是获得单元测试可更改系统时间的最方法。
6赞 ShloEmi 4/6/2015
恕我直言,最好使用接口而不是全局静态单例。考虑以下场景:测试运行程序是高效的,并且尽可能多地运行测试是并行的,一个测试更改为给定时间为 X,另一个测试更改为 Y。我们现在有一个冲突,这两个测试将切换失败。如果我们使用接口,每个测试都会根据自己的需要模拟接口,现在每个测试都与其他测试隔离。HTH。
12赞 Jay Bazuzi 9/9/2008 #6

单元测试成功的关键是解耦。你必须将你感兴趣的代码与它的外部依赖项分开,这样它才能被隔离地测试。(幸运的是,测试驱动开发可以生成解耦代码。

在本例中,外部是当前 DateTime。

我在这里的建议是将处理 DateTime 的逻辑提取到新方法或类或任何在您的情况下有意义的方法或类,然后传递 DateTime。现在,单元测试可以传入任意 DateTime,以生成可预测的结果。

5赞 Pawel Lesnikowski 2/2/2010 #7

我建议使用 IDisposable 模式:

[Test] 
public void CreateName_AddsCurrentTimeAtEnd() 
{
    using (Clock.NowIs(new DateTime(2010, 12, 31, 23, 59, 00)))
    {
        string name = new ReportNameService().CreateName(...);
        Assert.AreEqual("name 2010-12-31 23:59:00", name);
    } 
}

详细描述如下:http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

11赞 João Angelo 2/2/2010 #8

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

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

鼹鼠允许替换任何 .NET 方法。鼹鼠支架 静态或非虚拟方法。摩尔 依赖于 Pex 的分析器。

评论

0赞 Pandincus 3/11/2010
这很漂亮,但它需要 Visual Studio 2010!:-(
0赞 Torbjørn 7/6/2010
它在 VS 2008 中也运行良好。不过,它最适合 MSTest。你可以使用 NUnit,但我认为你必须使用一个特殊的测试运行程序来运行你的测试。
0赞 brianpeiris 1/24/2013
如果可能的话,我会避免使用鼹鼠(又名Microsoft假货)。理想情况下,它应该只用于尚无法通过依赖注入进行测试的遗留代码。
1赞 Ray Cheng 4/16/2013
@brianpeiris,使用Microsoft假货有什么缺点?
0赞 ChrisCW 3/15/2016
你不能总是DI第三方内容,所以如果你的代码调用了你不想在单元测试中实例化的第三方API,那么单元测试是完全可以接受的。我同意避免将假货/痣用于您自己的代码,但对于其他目的,它完全可以接受。
25赞 mmilleruva 1/3/2014 #9

使用 Microsoft Fakes 创建填充码是一种非常简单的方法。假设我有以下类:

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

}

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

Adding Fakes Assembly

最后,下面是测试类的样子:

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

        }
    }
}
}

评论

2赞 Douglas Ludlow 6/7/2014
这正是我想要的。谢谢!顺便说一句,在 VS 2013 中工作相同。
1赞 RJB 9/29/2016
或者现在,VS 2015 企业版。对于这样的最佳实践来说,这是一种耻辱。
4赞 tugberk 11/18/2014 #10

简单的答案:抛弃 System.DateTime :)相反,请使用 NodaTime 及其测试库:NodaTime.Testing

延伸阅读:

1赞 Pawel Wujczyk 6/27/2018 #11

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

public interface IDateTimeTools
{
    DateTime Now { get; }
}

当然,实现非常简单

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

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

enter image description here

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

Install-Package -Id DateTimePT -ProjectName Project

Nuget 的代码在这里

可以在此处找到 Autofac 的使用示例。

0赞 dragan.stepanovic 12/9/2022 #12

只需将 DateTime.Now 作为参数传递给需要它的方法即可。没有比这更简单的了。https://blog.thecodewhisperer.com/permalink/beyond-mock-objects

1赞 Tony Troeff 6/22/2023 #13

.NET 8 引入了另一种方法。你可以在这里阅读更多关于它的信息。

此外,Microsoft.Extensions.TimeProvider.Testing 公开了可在测试中使用的类。FakeTimeProvider

下面是一个示例:

public class ServiceClass
{
    private readonly TimeProvider _timeProvider;

    public TimeExperiments(TimeProvider timeProvider)
    {
        this._timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
    }

    public void ServiceMethod()
    {
        long timestamp = this._timeProvider.GetTimestamp();
        DateTimeOffset localTime = this._timeProvider.GetLocalNow();
        DateTimeOffset utcTime = this._timeProvider.GetUtcNow();
        
        // Later you can use time-dependent data throughout the `this._timeProvider` instance.
    }
}

这是如何使用:FakeTimeProvider

public void TestServiceMethod()
{
    // This creates a clock whose time is set to midnight January 1st 2000.
    var fakeTimeProvider = new FakeTimeProvider();
    var service = new ServiceClass(fakeTimePRovider);
    
    // Later you can use this service.
}

注意:这仍然是一项实验性功能,因为 .NET 8 仍处于预览状态。