2012-03-30 69 views
8

正常的解决方案是将其隐藏在接口后面。如何在单元测试中模拟DateTime.Now?

public class RecordService 
{ 
    private readonly ISystemTime systemTime; 

    public RecordService(ISystemTime systemTime) 
    { 
     this.systemTime = systemTime; 
    } 

    public void RouteRecord(Record record) 
    { 
     if (record.Created < 
      systemTime.CurrentTime().AddMonths(-2)) 
     { 
      // process old record 
     } 

     // process the record 
    } 
} 

在单元测试中,你可以使用模拟对象,并决定什么返回

[TestClass] 
public class When_old_record_is_processed 
{ 
    [TestMethod] 
    public void Then_it_is_moved_into_old_records_folder() 
    { 
     var systemTime = A.Fake<ISystemTime>(); 
     A.CallTo(() => system.Time.CurrentTime()) 
      .Returns(DateTime.Now.AddYears(-1)); 

     var record = new Record(DateTime.Now); 
     var service = new RecordService(systemTime); 

     service.RouteRecord(record); 

     // Asserts... 
    } 
} 

我不喜欢注入另一个接口到我的课堂只是为了获得当前的时间。对于这样一个小问题,感觉过于沉重。解决方案是使用静态类与公共功能。

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

现在我们可以删除ISystemTime注射RecordService看起来像这样

public class RecordService 
{ 
    public void RouteRecord(Record record) 
    { 
     if (record.Created < SystemTime.Now.AddMonths(-2)) 
     { 
      // process old record 
     } 

    // process the record 
    } 
} 

在单元测试中,我们可以很容易地嘲笑系统时间。

[TestClass] 
public class When_old_record_is_processed 
{ 
    [TestMethod] 
    public void Then_it_is_moved_into_old_records_folder() 
    { 
     SystemTime.Now =() => DateTime.Now.AddYears(-1); 
     var record = new Record(DateTime.Now); 
     var service = new RecordService(); 

     service.RouteRecord(record); 

     // Asserts... 
    } 
} 

当然,这一切都有一个缺点。您正在使用公共字段(The HORROR!),因此没有人阻止您编写这样的代码。

public class RecordService 
{ 
    public void RouteRecord(Record record) 
    { 
     SystemTime.Now =() => DateTime.Now.AddYears(10); 
    } 
} 

另外我认为,教育开发人员比创建抽象是为了保护他们免于犯错。其他可能的问题与运行测试有关。如果您忘记将功能恢复到原始状态,则可能会影响其他测试。这取决于单元测试运行器执行测试的方式。 您可以使用相同的逻辑来嘲笑文件系统操作

public static class FileSystem 
{ 
    public static Action<string, string> MoveFile = File.Move; 
} 

在实施这种利用公共职能的功能(嘲讽的时候,简单的文件系统操作),我的意见是完全可以接受的。它使得代码更易于阅读,减少了依赖性,并且很容易在单元测试中模拟。

+1

你用过moq吗? (或任何其他模拟框架)http://code.google.com/p/moq/ – Magrangs 2012-03-30 11:38:04

+0

我修正了您的代码的格式。请将它与您的版本进行比较,以了解如何正确执行此操作。 – 2012-03-30 11:41:04

+1

@Magrangs:与所有其他“正常”嘲讽框架一样,Moq不能嘲讽像“DateTime.Now”这样的静态方法和属性。 – 2012-03-30 11:42:07

回答

3

你不需要手动实现这个。你可以使用Moles框架来做到这一点。 channel 9

+2

和moq ..和许多其他嘲讽框架:-) – Magrangs 2012-03-30 11:39:34

+0

事实上,这个问题让我感到惊讶,因为视频中痣的第一个例子是如何轻松地嘲笑DateTime.Now :) – daryal 2012-03-30 11:42:15

+1

@Magrangs:不正确。请参阅我对上述其他评论的回答。 – 2012-03-30 11:42:37

3

我不会说现在的时间是如此之小,如果您的应用程序变得国际化,并且在多个时区中使用,哪个时区是当前时间,最有可能您想要一个时区常用的时区使用,无论您的区域设置?

该接口允许您将这些知识抽象出来;我会考虑将“当前”时间作为一项相当复杂的任务来检索。

0

考虑到我们谈论的是惯用的c#,我不太理解接口背后仪式的问题。从本质上讲,您有一个TimeProvider,您将其作为依赖项注入到构造函数中,并为它提供了一个存根,以驱动相关测试代码的逻辑。

我不认为你的其他方法提供了好处,它们具有消极方面的不习惯性以及消除依赖注入的有用方面(记录依赖关系,嘲讽,控制反转等)

相关问题