如何在 ASP.NET Core 測試中操縱時間?

來源:互聯網
上載者:User

有時候,我們會遇到一些跟系統目前時間相關的需求,例如:

  • 只有開學季才允許錄入學生資訊
  • 只有到了晚上或者周六才允許備份部落格
  • 註冊滿 3 天的使用者才允許進行一些操作
  • 某使用者在 24 小時內被禁止發言

很顯然,要實現這些功能的代碼多多少少要用到 DateTime.Now 這個靜態屬性,然而要使用單元測試或者整合測試對上述需求進行驗證,往往需要採用一些曲線救國的方法甚至是直接跳過這些測試,這是因為在 .Net 中,DateTime.Now 通常難以被 Mock 。這時候我就要誇一誇 Angular 的測試載入器了,較完美的提供了 Date 對象的 Mock 方法,所以在編寫測試代碼的時候可以很容易的操縱 “目前時間”。

在網上一番查閱過後,我發現 .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 的替代品滿足以下需求:

  1. 由於測試案例往往是多線程並行隨機執行,所以替代品線上程間需要相互隔離
  2. 在整合測試中,ASP.NET Core 服務端代碼與測試代碼並不是運行在同一個線程中的,這時候,替代品需要能夠線上程中共用
  3. 能夠隨時的設定目前時間
  4. 在生產環境中,必須與 DateTime.Now 功能一致
  5. 替代品的簽名要與 DateTime.Now 一致

在爆棧網上的 這個答案的基礎上,我自己改造了一個在 ASP.NET Core 整合測試中可用的 SystemClock 類:

/// <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。

在測試代碼中,需要先手動調用 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,但是最近並沒有這麼多時間,所以先在這裡記著(挖坑預定)。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.