共用方式為


單元測試

單元測試驗證業務邏輯並防止迴歸。 持久的編排協調多項活動,且能迅速複雜化。 加入單元測試有助於你及早發現錯誤。

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

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

先決條件

  • xUnit — 測試框架
  • Microsoft.DurableTask.InProcessTestHost NuGet 套件

測試編排器功能

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

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

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

[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 並為每個活動呼叫設定期望回報值:

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

    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);
}