Day 23-Unit Test 应用於 DateTime-2 (情境及应用-3)

Unit Test 应用於 DateTime-前言-2

今天文章的内容是参考於 C# - how to inject, mock or stub DateTime for unit tests,今天为接续後面三个方法的探讨。


看程序码说故事 (DateTime-3)

我们在 Day-20 曾提到 Roy Osherove 在单元测试的艺术提过区域工厂的概念,其意思是在被测试类别针对要被抽离的物件撰写一个区域工厂方法,在写商业逻辑的时候可呼叫该方法新增物件。如此在撰写测试的时候,可写一个类别继承该类别并改写这个工厂方法的内容,已达到假物件注入的手法,同样的手法也可适用在 DateTime.Now,程序码如下:

public class Decision
{
    public string WhatToDo()
    {
        var currentDateTime = GetDateTime();

        if (currentDateTime.Hour > 8 && currentDateTime.Hour < 18)
        {
            return Work();
        }
        else if (currentDateTime.Hour > 18 && currentDateTime.Hour < 22)
        {
            return Exercise();
        }
        else
        {
            return Sleep();
        }
    }
    
    protected virtual DateTime GetDateTime()
    {
        return DateTime.Now;
    }

    private string Work()
    {
        return "Work!";
    }

    private string Exercise()
    {
        return "Exercise!";
    }

    private string Sleep()
    {
        return "Sleep!";
    }
}

因此,我们就可以写一只继承该类别的含假物件类别(撷取与覆写),如下:

public class StubDecision : Decision
{
    private readonly DateTime thisDateTime;

    public StubDecision(DateTime inThisDateTime)
    {
        thisDateTime = inThisDateTime;
    }

    protected override DateTime GetDateTime()
    {
        return thisDateTime;
    }
}

好,那最後就是测试码:

[Test]
public void WorkTest()
{
    // Arrange
    var decision = new StubDecision(new DateTime(2020, 01, 01, 08, 00, 00));
    
    // Act
    var whatToDo = decision.WhatToDo();
    
    // Assert
    Assert.Equal("Work!", whatToDo);
}

[Test]
public void ExerciseTest()
{
    // Arrange
    var decision = new StubDecision(new DateTime(2020, 01, 01, 18, 00, 00));
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

[Test]
public void SleepTest()
{
    // Arrange
    var decision = new StubDecision(new DateTime(2020, 01, 01, 23, 00, 00));
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

看程序码说故事 (DateTime-4)

本方法是采用 C# Func 的写法,Func 是委派手法中有回传值的(若想了解什麽是委派可查询关键字 delegate),因次我们可以在建构函式的时候 Func 手法,在撰写商业逻辑时,程序码中代入真实时间,而在测试时,透过委派的手法给予假时间,程序码如下:

public class Decision
{
    public string WhatToDo(Func<DateTime> getCurrentDateTime = null)
    {
        var currentDateTime = getCurrentDateTime == null ? DateTime.Now : getCurrentDateTime();
        
        if (currentDateTime.Hour > 8 && currentDateTime.Hour < 18)
        {
            return Work();
        }
        else if (currentDateTime.Hour > 18 && currentDateTime.Hour < 22)
        {
            return Exercise();
        }
        else
        {
            return Sleep();
        }
    }

    private string Work()
    {
        return "Work!";
    }

    private string Exercise()
    {
        return "Exercise!";
    }

    private string Sleep()
    {
        return "Sleep!";
    }
}

於是乎,在测试的时候就可以透过 Lambda 语法注入虚设常式,测试码如下:

[Test]
public void WorkTest()
{
    // Arrange
    var decision = new Decision();
    
    DateTimeWrapper.Set(new DateTime(2020, 01, 01, 08, 00, 00));
    
    // Act
    var whatToDo = decision.WhatToDo(() => new DateTime(2020, 01, 01, 10, 00, 00));
    
    // Assert
    Assert.Equal("Work!", whatToDo);
}

[Test]
public void ExerciseTest()
{
    // Arrange
    var decision = new Decision();
    
    DateTimeWrapper.Set(new DateTime(2020, 01, 01, 18, 00, 00));
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

[Test]
public void SleepTest()
{
    // Arrange
    var decision = new Decision();
    
    DateTimeWrapper.Set(new DateTime(2020, 01, 01, 23, 00, 00));
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

看程序码说故事 (DateTime-5)

那最後一种写法是使用 static 的方式,相较其他四种方式其缺点较明显如测试不可同时进行,在最後需要做 Reset 的动作等。不过,因其撰写手法简单,相信许多历史较悠久的测试码可以看到其踪影,所以还是值得观看其手法,并理解後可改写其他方式,来看看其原始码:

public class DateTimeWrapper{
    private static DateTime? dateTime;

    public static DateTime Now { get { return dateTime ?? DateTime.Now; } }

    public static void Set(DateTime setDateTime)
    {
        dateTime = setDateTime;
    }

    public static void Reset()
    {
        dateTime = null;
    }
}

public class Decision
{
    public string WhatToDo()
    {
        var currentDateTime = DateTimeWrapper.Now;
        
        if (currentDateTime.Hour > 8 && currentDateTime.Hour < 18)
        {
            return Work();
        }
        else if (currentDateTime.Hour > 18 && currentDateTime.Hour < 22)
        {
            return Exercise();
        }
        else
        {
            return Sleep();
        }
    }

    private string Work()
    {
        return "Work!";
    }

    private string Exercise()
    {
        return "Exercise!";
    }

    private string Sleep()
    {
        return "Sleep!";
    }
}

可以看出,我们呼叫 Now 是检查 DateTimeWrapper 类别里面 static dateTime 属性有没有值,没有的话即呼叫现在的时间,而测试码如下:

[Test]
public void WorkTest()
{
    // Arrange
    var decision = new Decision();
    
    DateTimeWrapper.Set(new DateTime(2020, 01, 01, 08, 00, 00));
    
    // Act
    var whatToDo = decision.WhatToDo();
    
    // Assert
    Assert.Equal("Work!", whatToDo);
}

[Test]
public void ExerciseTest()
{
    // Arrange
    var decision = new Decision();
    
    DateTimeWrapper.Set(new DateTime(2020, 01, 01, 18, 00, 00));
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

[Test]
public void SleepTest()
{
    // Arrange
    var decision = new Decision();
    
    DateTimeWrapper.Set(new DateTime(2020, 01, 01, 23, 00, 00));
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

要注意的是,使用 static 方法要在最後撰写 TearDown 的方法,如下:

[TearDown]
public void TearDown()
{
    // 将 dateTime 重新设定为 null
    DateTimeWrapper.Reset();
}

不然下个测试如果没有撰写好设定时间,会用上一个测试的时间去跑结果 /images/emoticon/emoticon01.gif


Unit Test 应用於 DateTime-结尾

其实这五个方法看下来,大多就是在探讨如何接缝的问题,不同的手法都可以达到注入假物件的概念;除了第五个 static 手法考虑在记忆体有限的状况下可以使用,若平常没有记忆体的考量,大多采用其他四种。其中,若要程序码简洁且易扩充,可使用第二种假物件框架的手法;而重构时,大多可采用第三种策略,继承完并覆写的手法去撰写 Legacy Code 的测试码。


<<:  JavaScript Day08 - 物件

>>:  创建App-主页界面

[Day21] Load Balancer

Load Balancer (负载平衡器) 与 Auto Scaling 都算是云端设备中非常重要的...

Day 6 python集合

今天我们要介绍的是python的集合,所谓的集合就是指将元素用{}包住并且是没有顺序也不会重复的资料...

纠正不合规问题并减轻风险, 您最关心的项目为何?

基於风险的方法已广泛用於各个领域,例如决策,审计,网络安全,银行等。“风险是不确定性对目标的影响。”...

Day 1 初探Flutter

前言 大家好,我是辅大大三的学生,由於课程所需让我接触到铁人赛,因此我和同学一起组队参加,首先谢谢大...

DAY7 资料室--Vuex是个虾咪东东?

前言 曾几何时,你有没有对元件中资料调用感到困扰呢? 同阶层的元件资料传递,需要使用 Event B...