使用 MSTest 撰寫單元測試套路整理(3) - 拾遺補缺

前言

在前 2 篇文章中,我整理了自己常使用的測試技巧 (第一篇第二篇),本文繼續來整理一些實務撰寫測試上有用的小技巧,其中有些技巧是屬於 MSTest 這個測試框架專屬的行為。

目錄

6. 當程式使用到日期資訊時該如何進行測試?

我們在系統開發中,很常使用 DateTime 類別中的各種日期資訊,比方說我們在比對訂單是否逾期未出貨,或是客戶逾期未取貨等,都會使用到像 DateTime.Now 等屬性 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Order
{
/// <summary>
/// 下訂日期
/// </summary>
public DateTime OrderDate;

/// <summary>
/// 是否為逾期未取貨訂單
/// </summary>
public bool IsExpiredOrder
{
get
{
return (DateTime.Now.Subtract(this.OrderDate).Days >= 3);
}
}

// 略...
}

這時候當我們想要測試 2 種測試案例︰逾期訂單未逾期訂單時時,可以想像因為 DateTime.Now 是系統當下時間,所以我們很難照我們的期待,指定日期 (1/3 或 1/4) 來進行測試︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[TestClass]
public class OrderUnitTests
{
[TestMethod]
public void IsExpiredOrder_下單日為11號_今日日期為13號_視為未逾期訂單()
{
// 略...
}

[TestMethod]
public void IsExpiredOrder_下單日為11號_今日日期為14號_視為逾期訂單()
{
// 略...
}
}

這時候照之前的做法,我們可能會想在 Order 類別中開一個接口,將 DateTime.Now 由測試程式注入,但因為 DateTime 的用途非常多,如果每個使用到它的類別我們都這樣進行測試的話,並不是非常好的做法。

The Art of Unit Testing 一書中提出了一個不錯的方式,就是將 DateTime 類別給抽象化成一個新的 DateUtility 類別,並要求所有 Production Code 都改使用 DateUtility 類別來取得日期資訊︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Order
{
/// <summary>
/// 下訂日期
/// </summary>
public DateTime OrderDate;

/// <summary>
/// 是否為逾期未取貨訂單
/// </summary>
public bool IsExpiredOrder
{
get
{
return (DateUtility.Now.Subtract(this.OrderDate).Days >= 3);
}
}

// 略...
}

public class DateUtility
{
/// <summary>
/// 由測試程式指定日期的接口,可將可視範圍訂為 internal,確保只有測試程式可以存取此變數
/// </summary>
internal static DateTime? PredefindDate;

/// <summary>
/// 如果沒有指定日期,則直接回傳系統時間
/// </summary>
public static DateTime Now
{
get
{
return PredefindDate ?? DateTime.Now;
}
}
}

此時在我們的測試程式中,就可以針對目前的時間,多了更多的掌控性,測試程式寫法如下︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[TestClass]
public class OrderUnitTests
{
[TestMethod]
public void IsExpiredOrder_下單日為11號_今日日期為13號_視為未逾期訂單()
{
// Arrange
var order = new Order();
order.OrderDate = new DateTime(2017, 1, 1);

DateUtility.PredefindDate = new DateTime(2017, 1, 3);

// Act
var isExpiredOrder = order.IsExpiredOrder;

// Assert
Assert.AreEqual(false, isExpiredOrder);
}

[TestMethod]
public void IsExpiredOrder_下單日為11號_今日日期為14號_視為逾期訂單()
{
// Arrange
var order = new Order();
order.OrderDate = new DateTime(2017, 1, 1);

DateUtility.PredefindDate = new DateTime(2017, 1, 4);

// Act
var isExpiredOrder = order.IsExpiredOrder;

// Assert
Assert.AreEqual(true, isExpiredOrder);
}

[TestCleanup]
public void Cleanup()
{
DateUtility.PredefindDate = null;
}
}

如此針對日期資訊這種使用範圍如此廣的物件,我們就可以使用比較清楚的方式進行測試。針對這個議題,StackOverflow 上的 Unit Testing: DateTime.Now 討論串,也非常推薦一讀。

另外可以留意,我在上面的測試中加入 Cleanup 的程式,確保在每個 unit test 執行完成後,將 PredefinedDate 日期資訊給歸零,以避免各個 test case 間彼此間互相影響。

回到目錄

7. 如何部署測試需要的檔案?

有時候我們在測試前,需要部署測試需要的檔案。比方說未來我們會提到的 SQL Server LocalDB,通常在使用上,會先將測試資料庫 .mdf 檔先部署上測試資料夾,再啟動 LocalDB instance。這時候我們該怎麼設定呢?

在 MSTest,我們可以使用測試設定檔來達成這個目標。以下為 Visual Studio 的操作︰

假設我們的測試專案中,有個 TestMaterials\ 資料夾,內含我們所需要的測試檔︰

我們該怎麼設定讓 TestMaterials\ 資料夾的檔案被部署出去呢?首先我們為整個方案(Solution),新增一個測試設定檔︰

再來我們啓用部署測試檔案設定,並選擇將 TestMaterials\ 目錄下的所有檔案,給部署到測試資料夾中︰

這樣我們的檔案部署設定就完成了。

而在執行測試時,需要告訴 Visual Studio,以我們設定的測試設定來進行測試︰

但在測試前要留意一個小地方,因為如果測試沒有出現紅燈的話,MSTest 預設會在測試完成後,將測試相關檔案給刪除。為了証實我們的設定是有效的,我們可以加入一個一定會紅燈的測試案例︰

1
2
3
4
5
6
7
8
9
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Assert.AreEqual(true, false);
}
}

現在我們可以執行測試,看看結果如何。可以看到在整個方案目錄下,會多了一個 TestResults\ 資料夾︰

而整個 TestResults\ 資料夾的結構長得像這樣︰

我們可以觀察到,在每次測試時行的時候,MSTest 會執行以下動作︰

  1. 在方案的目錄下,建立一個 TestResults\ 資料夾
  2. 以目前的時間,建立一個測試用資料夾 (上圖中的 jonalin 開頭的資料夾),在測試用資料夾中建立 In\Out\ 子資料夾
  3. 將測試設定檔 (TestProject.testsettings) 中,設定要部署的檔案,複製到 Out\ 資料夾下

這時候我們可以在檔案總管中,瀏覽看看 Out\ 資料夾的內容,會發現我們指定的 Test.txt 檔案,已被複製到 Out\ 資料夾中︰

回到目錄

8. 如何讓測試以平行處理方式執行?

當我們測試程式越寫越多,這時候我們就會發現測試專案的執行時間越來越長,尤其當我們的測試程式不再單純是單元測試,而是牽扯到網路或是資料庫的整合測試時,測試程式執行的時間就會隨著測試案例數量的成長而拉長。我們有沒有什麼辦法可以加速測試專案的執行時間呢?

針對這個議題,MSTest 提供了讓測試專案可以平行處理執行(Parallel Execute) 的能力,讓我們先假設我們有 2 個測試案例,當中非常單純,就是使用 Thread.Sleep() API 各自睡眠 10 秒鐘︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Thread.Sleep(10000);
}

[TestMethod]
public void TestMethod2()
{
Thread.Sleep(10000);
}
}

這時候在沒有做任何設定的前提下,MSTest 會以單執行緒執行,此時執行測試專案時,我們可以預期整個專案的執行時間至少為 20 秒起跳︰

實驗的結果也如我們所預期,那我們該如何設定,讓 MSTest 可以多執行緒的方式來執行呢?參考 MSTest 的官方說明 Executing Unit Tests in parallel on a multi-CPU/core machine,我們可以在方案總管中,按右鍵編輯上一節 7. 如何部署測試需要的檔案? 所建立的 TestProject.testsettings 檔︰

並在 TestProject.testsettings 背後的 XML 中,找到 <Execution> 標籤,並加入 parallelTestCount="2" 屬性,要求 MSTest 以 2 個 thread 來執行我們的測試專案︰

再以上節說明的,讓 MSTest 使用我們的測設設定 TestProject.testsettings 來執行測試專案。執行結果如下,可以看到我們的執行時間由原本的 21 秒降低到了 14 秒,效果還滿顯著的︰

最後需要留意的是,雖然技術上我們可以平行處理方式來執行測試案例,但實際上要開啟這樣的設定,必須確保我們的測試案例,彼此間是 Thread-safe 的,這就進一步考驗我們對於整個測試專案的架構設計能力。

比方我們在 6. 當程式使用到日期資訊時該如何進行測試? 一節中,針對時間 DateTime 隔離的做法,在多執行緒的狀況下,測試案例間彼此可能就會互相干擾。這方面相關的考量非常多,在此就不展開,留待未來再做說明吧。

回到目錄

References

  1. The Art of Unit Testing
  2. Unit Testing: DateTime.Now
  3. How to: Deploy Files for Tests
  4. Executing Unit Tests in parallel on a multi-CPU/core machine

回到目錄

Proudly powered by Hexo and Theme by Hacker
© 2018 LittleLin