iT邦幫忙

2021 iThome 鐵人賽

DAY 22
0

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 比較一致,且不需要手刻虛設常式,程式碼較簡潔。


那因文章篇幅,其他的方式會在明天一一說明,並統整比較五個方法之前的效益。


上一篇
Day 21-Unit Test 應用於 Web APIs (情境及應用-1)
下一篇
Day 23-Unit Test 應用於 DateTime-2 (情境及應用-3)
系列文
單元測試從入門到進階之路 (以 C# NUnit 3 X NSubstitute 為例)30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言