單元測試驗證業務邏輯並防止迴歸。 持久的編排協調多項活動,且能迅速複雜化。 加入單元測試有助於你及早發現錯誤。
使用 Durable Functions 時,你可以透過模擬框架提供的上下文物件來測試編排器、活動和客戶端(觸發器)函式,並直接呼叫你的函式。 這種方法能將你的商業邏輯與 Azure Functions 執行時隔離開來。
獨立的 Durable Task SDK 提供 內建測試基礎設施 ,可在記憶體中執行編排,無需外部依賴。 您可以將協調器和活動註冊到測試背景工作、透過測試用戶端排程協調流程,並對結果進行判斷提示。 C# 和 JavaScript 不需要模擬。 Python 採用基於執行器的模擬歷史事件方法。
先決條件
- xUnit — 測試框架
- Moq — 模擬框架
- 熟悉.NET孤立工人模型
- xUnit — 測試框架
-
Microsoft.DurableTask.InProcessTestHostNuGet 套件
測試編排器功能
編排器功能協調活動、計時器及外部事件。 它們通常包含最多的商業邏輯,並從單元測試中獲益最大。
模擬協調流程內容,以控制活動呼叫的傳回值。 然後直接打電話給你的編排器確認輸出。
請考慮這個會呼叫活動三次的協調器:
[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);
}