對耐用函式與耐久任務 SDK 進行單元測試

單元測試持久性協調有助於你驗證業務邏輯並及早發現錯誤。 編排協調多項活動,且可能迅速複雜,因此測試能防止隨著工作流程演進而出現迴歸。

選擇與你專案相符的分頁:如果你使用 Azure Functions,請選擇 Durable Functions;若使用獨立 SDK 且未Azure Functions,則選擇 Durable Task SDK 。

使用 Durable Functions 時,你可以透過模擬框架提供的上下文物件來測試編排器、活動和客戶端(觸發器)函式,並直接呼叫你的函式。 這種方法能將你的商業邏輯與 Azure Functions 執行時隔離開來。

這裡有一個簡易的 C# 編排器測試來展示這個模式:

[Fact]
public async Task MyOrchestrator_CallsActivity()
{
    var contextMock = new Mock<TaskOrchestrationContext>();
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.IsAny<TaskName>(), It.IsAny<string>(), It.IsAny<TaskOptions>()))
        .ReturnsAsync("result");

    var result = await MyOrchestrator.Run(contextMock.Object);

    Assert.Equal("result", result);
}

本文其餘部分將詳細介紹 C# 與 Python 的這個模式。

獨立的 Durable Task SDK 提供 內建測試基礎設施 ,可在記憶體中執行編排,無需外部依賴。 您可以將協調器和活動註冊到測試背景工作、透過測試用戶端排程協調流程,並對結果進行判斷提示。 C# 和 JavaScript 不需要模擬。 Python 採用以產生器為基礎的方法,並透過手動結果注入。

這裡有一個簡易的 C# 測試來展示這個模式:

[Fact]
public async Task MyOrchestrator_Completes()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<MyOrchestrator>();
        tasks.AddActivity<MyActivity>();
    });

    string id = await host.Client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestrator));
    var result = await host.Client.WaitForInstanceCompletionAsync(id, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);
}

本文其餘部分將詳細介紹 C#、Python 和 JavaScript 的這個模式。

先決條件

測試編排器功能

編排器功能協調活動、計時器及外部事件。 它們通常包含最多的商業邏輯,並從單元測試中獲益最大。

模擬協調流程內容,以控制活動呼叫的傳回值。 然後直接打電話給你的編排器確認輸出。

請考慮這個會呼叫活動三次的協調器:

[Function(nameof(HelloCitiesOrchestration))]
public static async Task<List<string>> HelloCities(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var outputs = new List<string>
    {
        await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"),
        await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"),
        await context.CallActivityAsync<string>(nameof(SayHello), "London")
    };

    return outputs;
}

使用 Moq 來模擬 TaskOrchestrationContext 並設定每個活動呼叫的期望回報值。

Note

It.Is<TaskName>(...)這個模式是必要的,因為它CallActivityAsync接受TaskName結構,而不是純字串。 Moq 需要明確的類型匹配。

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    var contextMock = new Mock<TaskOrchestrationContext>();

    // Mock each activity call to return a known value
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "Tokyo"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello Tokyo!");

    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "Seattle"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello Seattle!");

    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "London"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello London!");

    var result = await HelloCitiesOrchestration.HelloCities(contextMock.Object);

    Assert.Equal(3, result.Count);
    Assert.Equal("Hello Tokyo!", result[0]);
    Assert.Equal("Hello Seattle!", result[1]);
    Assert.Equal("Hello London!", result[2]);
}

用 DurableTaskTestHost 來在記憶體中執行編排。 註冊您的生產協調器和活動類別、排程協調流程,並對結果進行判斷提示。

給定這些生產類別:

class HelloCitiesOrchestrator : TaskOrchestrator<string, List<string>>
{
    public override async Task<List<string>> RunAsync(
        TaskOrchestrationContext context, string input)
    {
        var outputs = new List<string>
        {
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Tokyo"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Seattle"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "London")
        };
        return outputs;
    }
}

class SayHelloActivity : TaskActivity<string, string>
{
    public override Task<string> RunAsync(TaskActivityContext context, string name)
    {
        return Task.FromResult($"Hello {name}!");
    }
}

直接在測試主機中註冊:

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));
    OrchestrationMetadata result = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);

    var output = result.ReadOutputAs<List<string>>();
    Assert.Equal(3, output.Count);
    Assert.Equal("Hello Tokyo!", output[0]);
    Assert.Equal("Hello Seattle!", output[1]);
    Assert.Equal("Hello London!", output[2]);
}

DurableTaskTestHost 運行完整的記憶體內編排引擎。 不需要外部服務或 Sidecar 處理序。

測試活動功能

活動函式包含實際工作——呼叫 API、處理資料或與外部系統互動。 它們是最容易測試的函式類型,因為沒有架構特定的重新執行行為。

Azure Functions 中的活動函式會接收輸入,以及選擇性的 FunctionContext。 像測試其他函數一樣:

[Function(nameof(SayHello))]
public static string SayHello(
    [ActivityTrigger] string name, FunctionContext executionContext)
{
    return $"Hello {name}!";
}
[Fact]
public void SayHello_ReturnsExpectedGreeting()
{
    var result = HelloCitiesOrchestration.SayHello("Tokyo", Mock.Of<FunctionContext>());
    Assert.Equal("Hello Tokyo!", result);
}

活動函式接收上下文物件與輸入。 上下文會提供像是編排 ID 和任務 ID 這類的元資料,但大多數測試並不需要。

在編排器範例中使用 SayHelloActivity 類別,直接以模擬的上下文呼叫 RunAsync:

[Fact]
public async Task SayHello_ReturnsExpectedGreeting()
{
    var activity = new SayHelloActivity();
    var contextMock = new Mock<TaskActivityContext>();

    var result = await activity.RunAsync(contextMock.Object, "Tokyo");

    Assert.Equal("Hello Tokyo!", result);
}

當你使用 DurableTaskTestHost時,活動也會作為編排測試的一部分執行。 除非活動邏輯複雜,否則不需要另外的活動測試。

測試用戶端功能

用戶端函式(也稱為觸發函式)啟動編排並管理實例。 他們使用持久客戶端綁定來與編排引擎互動。

考慮這個啟動編排的 HTTP 觸發器:

[Function("HelloCitiesOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    FunctionContext executionContext)
{
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestration));
    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

模擬 DurableTaskClient 以回傳已知的實例 ID:

[Fact]
public async Task HttpStart_ReturnsAccepted()
{
    var durableClientMock = new Mock<DurableTaskClient>("testClient");
    var functionContextMock = new Mock<FunctionContext>();
    var instanceId = "test-instance-id";

    durableClientMock
        .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(
            It.IsAny<TaskName>(),
            It.IsAny<object>(),
            It.IsAny<StartOrchestrationOptions>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(instanceId);

    var mockRequest = CreateMockHttpRequest(functionContextMock.Object);

    var responseMock = new Mock<HttpResponseData>(functionContextMock.Object);
    responseMock.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.Accepted);

    durableClientMock
        .Setup(x => x.CreateCheckStatusResponseAsync(
            It.IsAny<HttpRequestData>(),
            It.IsAny<string>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(responseMock.Object);

    var result = await HelloCitiesOrchestration.HttpStart(
        mockRequest, durableClientMock.Object, functionContextMock.Object);

    Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);
}

測試客戶端操作

使用獨立的耐久任務 SDK 時,客戶端作業 (排程協調流程、查詢狀態、引發事件) 會使用與協調器測試中已經展示的相同TestOrchestrationClient。 沒有獨立的客戶端函式——你直接呼叫客戶端 API。

DurableTaskTestHost 公開host.Client,這是一個功能完整的DurableTaskClient。 用它來測試用戶端層級的操作,例如排程、查詢或終止編排:

[Fact]
public async Task Client_CanQueryOrchestrationStatus()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));

    // Query status while the orchestration runs
    OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus);
    Assert.Equal(instanceId, metadata.InstanceId);
}