你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
单元测试持久性编排有助于验证业务逻辑并尽早捕获错误。 编排协调多个活动,很快变得复杂,因此测试可以在工作流发展的同时防止回归。
如果使用的是 Azure Functions,请选择与项目匹配的选项卡:Durable Functions;如果使用不带Azure Functions的独立 SDK,请选择可操作的任务 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 并设置每个活动调用的预期返回值。
注释
该 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 运行完整的内存业务流程引擎。 不需要外部服务或边车进程。
测试活动函数
活动函数包含实际工作 - 调用 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);
}