有時候,咱們會遇到一些跟系統當前時間相關的需求,例如:git
很顯然,要實現這些功能的代碼多多少少要用到 DateTime.Now
這個靜態屬性,然而要使用單元測試或者集成測試對上述需求進行驗證,每每須要採用一些曲線救國的方法甚至是直接跳過這些測試,這是由於在 .Net 中,DateTime.Now
一般難以被 Mock 。這時候我就要誇一誇 Angular 的測試工具了,較完美的提供了 Date
對象的 Mock 方法,因此在編寫測試代碼的時候能夠很容易的操縱 「當前時間」。github
在網上一番查閱事後,我發現 .Net FrameWork 中曾經是有這樣的工具的,不只僅是 Mock DateTime.Now
,其餘的不少來自於 mscorlib.dll
的方法、屬性也能夠被 Mock。這類工具根據工做原理大體分爲三類,第一類是提供了一個生成假 mscorlib.dll
的方法,而後再把生成出來的假的 dll 添加到測試項目中,第二類則是在運行時建立一個獨立的 AppDomain
,而後在這個 AppDomain
中加載程序集的時候臨時生成一個內存中的假程序集替換進去,還有一種則是直接在運行時修改目標函數/屬性的引用地址。這三種解決方案中,我我的更傾向於第二種 —— 更加靈活,並且不會改變現有流程。不過,這些搜索到的結果基本上都是面向 .Net Framework
開發的,能支持 .Net Core 並且不收費的工具,我如今還沒找到。如今我在關注的是 Smocks 這個項目,也嘗試過把他遷移到 .Net Core 上,結果由於 netstandard 中缺乏必要 API 而了結,看微軟的開發進度,他們估計要到 .Net Core 3.0 纔會補上這些 API,這個項目能等,但我手頭上的項目等不起啊,沒辦法,只能先拙劣的替換DateTime.Now
來實現相似的功能了。多線程
DateTime.Now
?一個合格的 DateTime.Now
的替代品知足如下需求:app
DateTime.Now
功能一致DateTime.Now
一致在爆棧網上的 這個答案的基礎上,我本身改造了一個在 ASP.NET Core 集成測試中可用的 SystemClock
類:async
/// <summary> /// Provides access to system time while allowing it to be set to a fixed <see cref="DateTime"/> value. /// </summary> /// <remarks> /// This class is thread safe. /// </remarks> public static class SystemClock { private static readonly Func<DateTime> Default = () => DateTime.Now; public static ThreadLocal<string> ClockId = new ThreadLocal<string>(() => "prod"); public static Dictionary<string, Func<DateTime>> ClocksMap = new Dictionary<string, Func<DateTime>>() { ["prod"] = Default }; private static DateTime GetTime() { var fn = ClocksMap[ClockId.Value] ?? Default; return fn(); } /// <inheritdoc cref="DateTime.Today"/> public static DateTime Today => GetTime().Date; /// <inheritdoc cref="DateTime.Now"/> public static DateTime Now => GetTime(); /// <inheritdoc cref="DateTime.UtcNow"/> public static DateTime UtcNow => GetTime().ToUniversalTime(); /// <summary> /// Sets a fixed (deterministic) time for the current thread to return by <see cref="DateTime"/>. /// </summary> public static void Set(DateTime time) { if (time.Kind != DateTimeKind.Local) time = time.ToLocalTime(); ClocksMap[ClockId.Value] = () => time; } /// <summary> /// Initialize clock with an id, so that you can share the clock across threads. /// </summary> /// <param name="clockId"></param> public static void Init(string clockId) { ClockId.Value = clockId; if (ClocksMap.ContainsKey(clockId) == false) { ClocksMap[clockId] = Default; } } /// <summary> /// Resets <see cref="SystemClock"/> to return the current <see cref="DateTime.Now"/>. /// </summary> public static void Reset() { ClocksMap[ClockId.Value] = Default; } }
在產品代碼中,須要手動的把全部的 DateTime.Now
替換成 SystemClock.Now
。ide
在測試代碼中,須要先手動調用 SystemClock.Init(clockId)
來進行初始化,它會把傳入的 clockId
存儲爲一個當前線程中的一個靜態變量,同時爲這個 Id 設置一個單獨的返回 DateTime
的委託。在使用 SystemClock.Now
的時候,它會尋找當前線程中 ClockId
對應的委託並返回執行結果。這樣,只要多個線程中的 SystemClock.Init
是經過一樣的 clockId
調用的,咱們就能夠在這些線程的任意一箇中共享或者設置 SystemClock.Now
的返回結果,而不一樣的線程中,若是 ClockId,那麼他們的 SystemClock.Now
相互不受影響。函數
舉個例子,假設有一個 TestStartup.cs
,爲了可以讓咱們在測試用例代碼執行的線程中修改 Controller
執行線程中 SystemClock.Now
的執行結果,首先須要設置一下 Configure
方法:工具
public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // TestStartup.Configure 會在測試線程中調用 var clockId = Guid.NewGuid().ToString(); SystemClock.Init(clockId); app.Use(async (context, next) => { // 中間件的執行線程與測試線程不一樣但與 Controller、Service 的執行線程相同 SystemClock.Init(clockId); await next(); }); }
因爲每次處理咱們請求的線程可能並非同一個,因此我就在第一個中間件中添加了初始化 SystemClock
的代碼。在測試用例中,咱們就能夠操縱時間了:單元測試
public async void SomeTest() { var now = new DateTime(2022,1,1); SystemClock.Set(now); // 註冊用戶 // Assert: 用戶還不能夠發言 var threeDaysAfter = now.AddDays(3); SystemClock.Set(threeDaysAfter); // Assert: 用戶能夠發言了
因爲手動替換 DateTime.Now
對現有代碼改動很大,因此上面提出的只是一個簡單的臨時應對方案。但要解決這個問題其實也不是很難,能夠嘗試在 dotnet build
以後把生成出來的全部 dll 經過工具處理一遍,在編譯的結果中替換 DateTime.Now
,可是最近並無這麼多時間,因此先在這裏記着⛏(挖坑預約)。測試