我有一些(C#)代码依赖于今天的日期来正确计算将来的事情。 如果我在测试中使用今天的日期,则必须在测试中重复计算,感觉不对。 在测试中将日期设置为已知值的最佳方法是什么,以便我可以测试结果是否为已知值?
我的首选是让使用时间的类实际上依赖于一个接口,例如
1 2 3 4
| interface IClock
{
DateTime Now { get; }
} |
具体实施
1 2 3 4
| class SystemClock: IClock
{
DateTime Now { get { return DateTime.Now; } }
} |
然后,如果需要,您可以提供任何其他想要测试的时钟,例如
1 2 3 4
| class StaticClock: IClock
{
DateTime Now { get { return new DateTime(2008, 09, 3, 9, 6, 13); } }
} |
向依赖它的类提供时钟可能会有一些开销,但是可以通过任何数量的依赖项注入解决方案来处理(使用控制容器反转,普通的旧构造函数/ setter注入,甚至是静态网关模式) )。
交付提供所需时间的对象或方法的其他机制也可以使用,但是我认为关键是要避免重置系统时钟,因为这只会给其他层次带来麻烦。
另外,使用DateTime.Now并将其包含在计算中并不会带来不适感-它使您失去了测试特定时间的能力,例如,如果您发现仅在午夜边界或星期二发现的错误。使用当前时间将无法测试这些情况。或至少不在您想要的时候。
Ayende Rahien使用一种非常简单的静态方法...
1 2 3 4
| public static class SystemTime
{
public static Func<DateTime> Now = () => DateTime.Now;
} |
我认为为诸如获取当前日期之类的简单事件创建单独的时钟类有点过头了。
您可以将今天的日期作为参数传递,以便可以在测试中输入其他日期。这具有使代码更灵活的附加好处。
使用Microsoft Fakes创建垫片是一种非常简单的方法。假设我有以下课程:
1 2 3 4 5 6 7 8
| public class MyClass
{
public string WhatsTheTime()
{
return DateTime.Now.ToString();
}
} |
在Visual Studio 2012中,可以通过右键单击要为其创建Fakes / Shims的程序集并选择"添加Fakes程序集",将Fakes程序集添加到测试项目中。
最后,这是测试类的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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的逻辑提取到新方法或类或您认为合适的任何东西,然后传递DateTime。现在,您的单元测试可以传递任意DateTime,以产生可预测的结果。
另一个使用Microsoft Moles(.NET的隔离框架)。
1
| MDateTime.NowGet = () => new DateTime(2000, 1, 1); |
Moles allows to replace any .NET
method with a delegate. Moles supports
static or non-virtual methods. Moles
relies on the profiler from Pex.
我建议使用IDisposable模式:
1 2 3 4 5 6 7 8 9
| [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/
简单的答案:抛弃System.DateTime :)而是使用NodaTime及其测试库:NodaTime.Testing。
进一步阅读:
-
使用Noda Time进行单元测试
-
依赖注入:注入您的依赖,周期!
您可以在测试的类中注入用于DateTime.Now的类(更好:方法/委托)。将DateTime.Now作为默认值,并且仅在测试中将其设置为返回常量值的伪方法。
编辑:布莱尔·康拉德(Blair Conrad)说了什么(他有一些代码要看)。除此之外,我倾向于使用委托,因为他们不会用IClock之类的东西弄乱您的类层次结构。
我经常遇到这种情况,以至于我创建了一个简单的nuget,它通过接口公开了Now属性。
1 2 3 4
| public interface IDateTimeTools
{
DateTime Now { get; }
} |
实施当然很简单
1 2 3 4
| public class DateTimeTools : IDateTimeTools
{
public DateTime Now => DateTime.Now;
} |
因此,在将nuget添加到我的项目中之后,我可以在单元测试中使用它
您可以直接从GUI Nuget软件包管理器或使用以下命令来安装模块:
1
| Install-Package -Id DateTimePT -ProjectName Project |
Nuget的代码在这里。
可在此处找到Autofac用法的示例。
您是否考虑过使用条件编译来控制调试/部署期间发生的情况?
例如
1 2 3 4 5 6
| DateTime date;
#if DEBUG
date = new DateTime(2008, 09, 04);
#else
date = DateTime.Now;
#endif |
否则,您想公开该属性以便可以对其进行操作,这是编写可测试代码的全部挑战,这是我目前正在努力解决的问题:D
编辑
我很大一部分会喜欢布莱尔的方法。这使您可以"热插拔"部分代码以帮助测试。一切都遵循设计原理封装各种测试代码与生产代码没有什么不同,只是没有人从外部看到过它。
对于本示例,创建和接口似乎需要很多工作(这就是为什么我选择条件编译的原因)。