如何在单元测试中模拟DateTime.Now?

时间:2012-03-30 11:35:35

标签: c# unit-testing mocking

正常的解决方案是将其隐藏在界面后面。

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

在我看来,使用公共函数实现这种功能(模拟时间,简单的文件系统操作)是完全可以接受的。它使代码更容易阅读,减少了依赖性,并且很容易在单元测试中进行模拟。

3 个答案:

答案 0 :(得分:3)

您无需手动实现此功能。您可以使用Moles框架来执行此操作。 channel 9

答案 1 :(得分:3)

我不会说当前时间是如此小的任务,如果您的应用程序变为国际化并且在多个时区使用,当您获得当前时间的时区时,很可能您会想要一个共同的时区使用,无论你的地方?

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

答案 2 :(得分:0)

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

我不相信你的其他方法提供了好处,并且它们具有不被惯用的消极方面以及消除依赖注入的有用方面(记录依赖性,模拟,控制反转等等)< / p>