單元測試持久性協調有助於你驗證業務邏輯並及早發現錯誤。 編排協調多項活動,且可能迅速複雜,因此測試能防止隨著工作流程演進而出現迴歸。
選擇與你專案相符的分頁:如果你使用 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 的這個模式。
先決條件
- xUnit — 測試框架
- Moq — 模擬框架
- 熟悉.NET孤立工人模型
- xUnit — 測試框架
-
Microsoft.DurableTask.InProcessTestHostNuGet 套件(v1.0.0 或更新版本)
測試編排器功能
編排器功能協調活動、計時器及外部事件。 它們通常包含最多的商業邏輯,並從單元測試中獲益最大。
模擬協調流程內容,以控制活動呼叫的傳回值。 然後直接打電話給你的編排器確認輸出。
請考慮這個會呼叫活動三次的協調器:
[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);
}