[Hangfire] 如何對 Hangfire Job 撰寫測試

寫測試可以縮短除錯、驗證時間,進而縮短整個開發時程,尤其是這種非同步的需求,如果沒有用測試技巧,真的會花費很多的時間,接下來就來分享使用 Hangfire 的測試技巧...

開發環境

  • VS 2019
  • .NET Framework 4.8

實作

開啟一個 UnitTest Project

安裝以下套件

Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
Install-Package Microsoft.Owin.Diagnostics
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Hangfire
Install-Package Hangfire.Console
Install-Package Hangfire.MemoryStorage
Install-Package Hangfire.Dashboard.Management
Install-Package NSubstitute

單元測試

隔離 IBackgroundJobClient

將任務加入排程內有兩種做法,一種是使用靜態方法﹑如下所示:

  • BackgroundJob.Enqueue
  • BackgroundJob.Schedule
  • BackgroundJob.ContinueWith

另一種是使用 IBackgroundJobClientBackgroundJob 骨子裏面是用 IBackgroundJobClient 實作的,若是要讓撰寫單元測試,應該讓你的物件相依 IBackgroundJobClient,下面這個例子,EnqueueAction 方法依賴了 JobClient.Enqueue 方法,JobClient.Enqueue 呼叫 Action 方法,Action 是上一篇介紹 Hangfire.Dashboard.Management 的範例程式,主要是用來給 Hangfire UI 操作介面觸發排程,EnqueueAction 則是用 JobClient.Enqueue 直接觸發排程。

[ManagementPage("演示", "default")]
public class DemoJob
{
	public IBackgroundJobClient JobClient
	{
		get
		{
			if (this._jobClient == null)
			{
				this._jobClient = new BackgroundJobClient();
			}

			return this._jobClient;
		}
		set => this._jobClient = value;
	}

	private IBackgroundJobClient _jobClient;

	public DemoJob()
	{
	}

	public DemoJob(IBackgroundJobClient jobClient)
	{
		this.JobClient = jobClient;
	}

	[Queue("default")]
	[Hangfire.Dashboard.Management.Support.Job]
	[DisplayName("呼叫內部方法")]
	[Description("呼叫內部方法")]
	[AutomaticRetry(Attempts = 3)]   //自動重試
	[DisableConcurrentExecution(90)] //禁止使用並行
	public void Action(PerformContext context = null, IJobCancellationToken cancellationToken = null)
	{
		if (cancellationToken.ShutdownToken.IsCancellationRequested)
		{
			return;
		}
		Console.WriteLine("Action 方法被調用");

		context.WriteLine($"測試用,Now:{DateTime.Now}");
	}


	public void EnqueueAction()
	{
		this.JobClient.Enqueue(() => this.Action(null, JobCancellationToken.Null));
	}
}

 

用 NSub 建立出假物件,呼叫被側方法,最後用 client.Received() 驗證 IBackgroundJobClient.Create 是否有調用 Action 方法

[TestMethod]
public void 驗證有呼叫Create方法()
{
    //arrange
    var client  = Substitute.For<IBackgroundJobClient>();
    var demoJob = new DemoJob(client);
 
    //act
    demoJob.EnqueueAction();
 
    //assert
    client.Received()
          .Create(Arg.Is<Job>(p => p.Method.Name    == "Action"),
                  Arg.Is<EnqueuedState>(p => p.Name == "Enqueued"));
}

 

集成測試

因為演示使用方式,我就不寫比對 (Assert) 了

只有測試 IBackgroundJobClientAction 的互動還不夠,因為 Action 並沒有真正的跑到,要怎麼讓測試案例直接測到 Action 方法?

  • 直接測 Action 方法,由於 Action 方法還依賴了 Hangfire 的物件,所以必須要解除依賴才可以,這我就不演練了。

  • 把 BackgroundJobServer 架起來,然後用 IBackgroundJobClient 建立排程。

BackgroundJobServer

下面的例子

  1. BackgroundJobServer 把 Hangfire Server 建立起來。
  2. 用 BackgroundJobClient 調用 DemoJob.EnqueueAction 方法。
  3. BackgroundJobServerBackgroundJobClient 這兩個都用到了相同的 JobStorage
  4. 由於這是非同步的服務,所以我讓 Server 的輪詢時間為 0 秒 SchedulePollingInterval = new TimeSpan(0, 0, 0),測試案例跑慢一點 Thread.Sleep(5000)
[TestClass]
public class BackgroundJobServer_IntegrateTest
{
    private static BackgroundJobServer s_jobServer;
    private static JobStorage s_storage;
 
    [ClassInitialize]
    public static void ClassInitialize(TestContext context)
    {
        var options = new BackgroundJobServerOptions
        {
            SchedulePollingInterval = new TimeSpan(0, 0, 0),
        };
        s_storage = new MemoryStorage();
        s_jobServer = new BackgroundJobServer(options, s_storage);
    }
 
    [ClassCleanup]
    public static void ClassCleanup()
    {
        s_jobServer?.Dispose();
    }
 
    [TestMethod]
    public void 集成測試()
    {
        var job    = new DemoJob();
        var client = new BackgroundJobClient(s_storage);
        job.JobClient = client;
        job.EnqueueAction();
        Thread.Sleep(5000);
    }
}

 

看測試報告驗證被測方法有被執行到。

再次提醒這是演示,正式來請加上驗證

 

BackgroundJobServer via OWIN

這裡我改用 OWIN  WebApp.Start 把 BackgroundJobServer 建立起來,其他的就跟上面的例子差不多,就不浪費篇幅囉

[TestClass]
public class BackgroundJobServerOwin_IntegrateTest
{
    private const  string      HOST_ADDRESS = "http://localhost:9527";
    private static IDisposable s_webApp;

    [ClassCleanup]
    public static void ClassCleanup()
    {
        s_webApp?.Dispose();
    }

    [ClassInitialize]
    public static void ClassInitialize(TestContext context)
    {
        s_webApp = WebApp.Start<Startup>(HOST_ADDRESS);
        Console.WriteLine("Hangfire service start...");
    }

    [TestMethod]
    public void 集成測試()
    {
        var job    = new DemoJob();
        var client = new BackgroundJobClient();
        job.JobClient = client;
        job.EnqueueAction();
        Thread.Sleep(5000);
    }
}

 

參考資源

https://docs.hangfire.io/en/latest/background-methods/writing-unit-tests.html

範例程式

https://github.com/yaochangyu/sample.dotblog/tree/master/Hangfire/Lab.HangfireUnitTes

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo