Day 22-Unit Test 应用於 DateTime-1 (情境及应用-2)

Unit Test 应用於 DateTime-前言

今天文章的内容是参考於 C# - how to inject, mock or stub DateTime for unit tests,在 C# 中 DateTime 是专门帮我们处理时间的资料结构,其好处是可以帮我们纪录年月日时分秒,也可以透过程序呼叫现在的时间点,但呼叫现在的时间这点会成为单元测试中不稳定的因素;因此,今天的议题就是来探讨如何在撰写 DateTime.Now 的情境中抽离并验证其商业逻辑是否正确,以下为使用 DateTime.Now 的范例:

public class Decision
{
    public string WhatToDo()
    {
        var currentDateTime = DateTime.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!";
    }
}

而截选本文主要原因是里面运用了写实务上系统很常用的 DateTime.Now 外,另一方面也提出了许多接缝的手法。所以接下来来探讨如何抽离 DateTime.Now 的手法吧!而本文提出了五种方向,如下:

  1. 设计新类别并搭配依赖注入手法
  2. 使用属性注入搭配假物件框架 (Day-19)
  3. 透过继承的方式做 Injection (Day-20)
  4. 使用 Func (全新)
  5. 使用 Static (全新)

那我们逐一来看吧~


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

第一个为新增时间包覆器的类别 —— DateTimeWrapper,里面包含 thisDateTime 属性、两种建构函式与 Now 方法,如下:

public class DateTimeWrapper
{
    private DateTime? thisDateTime;

    public DateTimeWrapper()
    {
        thisDateTime = null;
    }

    public DateTimeWrapper(DateTime fixedDateTime)
    {
        thisDateTime = fixedDateTime;
    }

    public DateTime Now { get { return thisDateTime ?? DateTime.Now; } }
}

因此,就可以开始撰写商业逻辑,如下:

public class Decision
{
    private readonly DateTimeWrapper thisDateTimeWrapper;

    public Decision(DateTimeWrapper inThisDateTimeWrapper)
    {
        thisDateTimeWrapper = inThisDateTimeWrapper;
    }

    public string WhatToDo()
    {
        var currentDateTime = thisDateTimeWrapper.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!";
    }
}
[Test]
public void WorkTest()
{
    // Arrange
    var dateTimeWrapper = new DateTimeWrapper(new DateTime(2020, 01, 01, 08, 00, 00));
    var decision = new Decision(dateTimeWrapper);
    
    // Act
    var whatToDo = decision.WhatToDo();
    
    // Assert
    Assert.Equal("Work!", whatToDo);
}

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

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

这样写好处在於,若处理既有程序码,可更动少量的程序码即达到做为测试的接缝。


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

第二个为假物件框架 (NSubstitute),就要提到我们最爱提的把商业逻辑介面化,从前言可看出 DateTime.Now 已经和 WhatToDo 耦合再一起,若要使用虚设常式抽离并实作,则先需新增一个时间的介面,如下:

public interface IDateTimeWrapper
{
    public DateTime Now { get { return DateTime.Now; } }
}

public class DateTimeWrapper : IDateTimeWrapper {}

因此,商业逻辑就改写如下:

public class Decision
{
    private readonly IDateTimeWrapper DateTimeWrapper;

    public Decision(IDateTimeWrapper inDateTimeWrapper)
    {
        DateTimeWrapper = inDateTimeWrapper;
    }

    public string WhatToDo()
    {
        var currentDateTime = DateTimeWrapper.Now;

        // ... 後面都一样
    }

    // ... 後面都一样
}

因此,就可以撰写测试码,而撰写测试码时,IDateTimeWrapper 的 Now 就可利用 NSubstitute 中的 Returns 注入相对应的属性,如下:

[Test]
public void WorkTest()
{
    // Arrange
    var dateTimeWrapper = Substitute.For<IDateTimeWrapper>();
    dateTimeWrapper.Now.Returns(new DateTime(2020, 01, 01, 08, 00, 00));
    
    var decision = new Decision(dateTimeWrapper);
    
    // Act
    var whatToDo = decision.WhatToDo();
    
    // Assert
    Assert.Equal("Work!", whatToDo);
}

[Test]
public void ExerciseTest()
{
    // Arrange
    var dateTimeWrapper = Substitute.For<IDateTimeWrapper>();
    dateTimeWrapper.Now.Returns(new DateTime(2020, 01, 01, 18, 00, 00));
    
    var decision = new Decision(dateTimeWrapper);
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

[Test]
public void SleepTest()
{
    // Arrange
    var dateTimeWrapper = Substitute.For<IDateTimeWrapper>();
    dateTimeWrapper.Now.Returns(new DateTime(2020, 01, 01, 23, 00, 00));
    
    var decision = new Decision(dateTimeWrapper);
    
    // Act + Assert
    // ... 结构与 WorkTest 都一样
}

这种撰写方式与我们先前所写的 Code Style 比较一致,且不需要手刻虚设常式,程序码较简洁。


那因文章篇幅,其他的方式会在明天一一说明,并统整比较五个方法之前的效益。


<<:  离职倒数9天:铺好轨道的人生

>>:  22.unity读取文字文件并分行(TextAsset、Split)

Router

路由架构 Breeze 已经架构好利用 inertia.js 取得 Login 等画面的路由,不过为...

Day7 认识Components与 Props

Components基本定义 Components是react组成的最基本元素,每一个Compont...

Day10 Let's ODOO: View(3) Search View

Search View在Odoo内非常常见,可以帮助使用者快速搜寻、过滤、分类需要的资料,因此透过设...

ES6 常用方法

1. 语法糖 、...展开 https://codepen.io/Rouoxo/pen/ZEXXda...

Day 03 「要开始罗!」单元测试的起手式:人生第一个单元测试

终於要开始了:「说到底,单元测试怎麽做?」 单元测试 单元测试要测的是一个逻辑单元功能是否正确。这短...