初寫單元測試的工程師,經常會問到如何解決測試目標中使用外部方法,如系統時間(DateTime.Now)。
本篇介紹如何透過自製包裝或 Microsoft Fakes 解決單元測試使用外部方法。
以 DateTime.Now 為例,假設以下程式碼是要被測試的目標:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public decimal Discount(decimal amount) { var now = DateTime.Now; if (now.Month == 1 && now.Day == 1) { return amount * 0.15m; } else if (now.Month == 12 && now.Day == 25) { return amount * 0.2m; } else if (now.DayOfWeek == DayOfWeek.Sunday) { return amount * 0.05m; } return 0; }
|
Interface 包裝
Interface 包裝是常見的控制翻轉 (Inversion Of Control, IoC) 方法,透過介面切斷相依,在測試方法中重新實作介面模擬回傳。
建立一個 ITimeWrapper 介面及 TimeWrapper 類別重新包裝 DateTime。程式碼如下:
1 2 3 4 5 6 7 8 9
| public interface ITimeWrapper { DateTime Now { get; } }
public class TimeWrapper : ITimeWrapper { public DateTime Now => DateTime.Now; }
|
被測試的目標改為:
1 2 3 4 5 6 7
| internal ITimeWrapper TimeWrapper = new TimeWrapper();
public decimal Discount(decimal amount) { var now = TimeWrapper.Now; }
|
自製包裝有一個不便之處,就是要改被測試的目標。也就是需要重構的意思。
測試程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class FakeTimeWrapper : ITimeWrapper { internal DateTime MockTime; public DateTime Now => MockTime; }
[Test] public void Christmas_Discount() { decimal expected = 1897.4m; decimal amount = 9487m; var fakeTimeWrapper = new FakeTimeWrapper(); fakeTimeWrapper.MockTime = Convert.ToDateTime("2017/12/25"); _target.TimeWrapper = fakeTimeWrapper;
var actual = _target.Discount(amount);
Assert.AreEqual(expected, actual); }
|
可以用 Mock Framework 簡化測試程式碼。
Static 包裝
Static 包裝的好處是不用宣告實體,用法更接近 DateTime 的使用方式。
建立一個 SystemTime 類別重新包裝 DateTime。程式碼如下:
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
| public static class SystemTime { private static Func<DateTime> _currentTime;
public static DateTime Now { get { if (_currentTime == null) { Reset(); } return _currentTime(); } internal set { _currentTime = () => value; } }
internal static void Reset() { _currentTime = () => DateTime.Now; } }
|
如此一來就可以在測試方法中,透過 internal set SystemTime.Now
模擬回傳值。
測試程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [Test] public void Christmas_Discount() { decimal expected = 1897.4m; decimal amount = 9487m; SystemTime.Now = Convert.ToDateTime("2017/12/25");
var actual = _target.Discount(amount);
Assert.AreEqual(expected, actual); }
|
測試專案要使用其他專案的 internal
可以參考這篇:C# 存取修飾詞 - internal
Microsoft Fakes
如果用 Visual Studio Enterprise 版本開發的話,可以透過 Microsoft Fakes 這個功能模擬外部參考提供的方法。
在專案的參考中,找到外部參考點右鍵,選擇新增 Fakes 組件:
新增 Fakes 組件完成後,編輯測試程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| using Microsoft.QualityTools.Testing.Fakes;
[Test] public void Christmas_Discount() { using (ShimsContext.Create()) { decimal expected = 1897.4m; decimal amount = 9487m; System.Fakes.ShimDateTime.NowGet = () => Convert.ToDateTime("2017/12/25");
var actual = Discount(amount);
Assert.AreEqual(expected, actual); } }
|
結論
- 自製包裝比較土法煉鋼的方式,優點是其他語言也能用此方法。缺點是必須要異動被測試目標。
- Microsoft Fakes 的好處是不用改原本的程式碼,缺點是口袋要夠深,畢竟要 Visual Studio Enterprise 版本才能用。
如果是新專案沒有歷史包袱的話,我會比較建議一開始就用自製包裝的方式,彈性會比較高。
舉例來說,我第一次上雲端,原本系統時間都是預期 GMT+08:00,上到 AWS 後才發現系統預設是 UTC,整個時間大亂,我只需要小改 SystemTime 就能解決此問題。如下:
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
| public static class SystemTime { private static readonly TimeZoneInfo _timeZone = TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time"); private static Func<DateTime> _currentTime;
public static DateTime Now { get { if (_currentTime == null) { Reset(); } return _currentTime(); } internal set { _currentTime = () => value; } }
internal static void Reset() { _currentTime = () => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _timeZone); } }
|
參考
使用 Microsoft Fakes 在測試期間隔離程式碼