在测试期间覆盖 DateTime.Now 的好方法是什么?
-
09-06-2019 - |
题
我有一些(C#)代码,它们依赖今天的日期来正确计算未来的事情。如果我在测试中使用今天的日期,我必须在测试中重复计算,这感觉不对。在测试中将日期设置为已知值以便我可以测试结果是否为已知值的最佳方法是什么?
解决方案
我的偏好是让使用时间的类实际上依赖于接口,例如
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
将其包含在您的计算中不仅感觉不对,而且还会剥夺您测试特定时间的能力,例如,如果您发现仅在午夜边界附近或周二发生的错误。使用当前时间将不允许您测试这些场景。或者至少不是在你想要的时候。
其他提示
艾延德·拉欣 用途 一个相当简单的静态方法......
public static class SystemTime
{
public static Func<DateTime> Now = () => DateTime.Now;
}
我认为为诸如获取当前日期之类的简单事情创建一个单独的时钟类有点矫枉过正。
您可以将今天的日期作为参数传递,以便您可以在测试中输入不同的日期。这样做的另一个好处是使您的代码更加灵活。
使用 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);
}
}
}
}
单元测试成功的关键是 解耦. 。您必须将您感兴趣的代码与其外部依赖项分开,以便可以单独测试它。(幸运的是,测试驱动开发产生解耦代码。)
在这种情况下,您的外部是当前的日期时间。
我的建议是将处理 DateTime 的逻辑提取到新方法或类或任何对您的情况有意义的内容,然后将 DateTime 传入。现在,您的单元测试可以传递任意日期时间,以产生可预测的结果。
另一种使用 Microsoft Moles (.NET 的隔离框架).
MDateTime.NowGet = () => new DateTime(2000, 1, 1);
痣允许用委托替换任何.NET方法。痣支持静态或非虚拟方法。痣依赖于PEX的剖面。
我建议使用 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/
您可以注入该类(更好:方法/代表)你用于 DateTime.Now
在被测试的班级中。有 DateTime.Now
是默认值,并且仅在测试时将其设置为返回常量值的虚拟方法。
编辑: Blair Conrad 说的话(他有一些代码可以看)。除此之外,我倾向于更喜欢代表,因为他们不会用类似的东西弄乱你的类层次结构 IClock
...
我经常遇到这种情况,所以我创建了简单的 nuget 来暴露 现在 通过接口的属性。
public interface IDateTimeTools
{
DateTime Now { get; }
}
实施当然非常简单
public class DateTimeTools : IDateTimeTools
{
public DateTime Now => DateTime.Now;
}
因此,将 nuget 添加到我的项目后,我可以在单元测试中使用它
您可以直接从 GUI Nuget Package Manager 或使用以下命令安装模块:
Install-Package -Id DateTimePT -ProjectName Project
Nuget 的代码是 这里.
可以找到 Autofac 的使用示例 这里.
您是否考虑过使用条件编译来控制调试/部署期间发生的情况?
例如
DateTime date;
#if DEBUG
date = new DateTime(2008, 09, 04);
#else
date = DateTime.Now;
#endif
如果做不到这一点,您想要公开该属性,以便可以操作它,这都是写作挑战的一部分 可测试的 代码,这是我目前正在努力解决的问题:D
编辑
我很大一部分人会更喜欢 布莱尔的做法. 。这允许您“热插拔”部分代码以帮助测试。这一切都遵循设计原则 封装变化的部分 测试代码与生产代码没有什么不同,只是没有人在外部看到它。
不过,对于这个示例来说,创建和接口似乎需要做很多工作(这就是我选择条件编译的原因)。