iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Software Development

單元測試從入門到進階之路 (以 C# NUnit 3 X NSubstitute 為例)系列 第 23

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 的測試碼。


上一篇
Day 22-Unit Test 應用於 DateTime-1 (情境及應用-2)
下一篇
Day 24-Unit Test 應用於 ORM (以 Entity Framework 為例) (情境及應用-4)
系列文
單元測試從入門到進階之路 (以 C# NUnit 3 X NSubstitute 為例)30

尚未有邦友留言

立即登入留言