单元测试是现代软件开发实践的重要组成部分。 单元测试验证业务逻辑行为,并防止将来引入未注意到的中断性变更。 Durable Functions 可以轻松增加复杂性,因此引入单元测试有助于避免重大更改。 以下部分介绍如何对三种函数类型(业务流程客户端、业务流程协调程序和活动函数)进行单元测试。
备注
- 本文为使用 C# 编写的 .NET 独立工作器中的 Durable Functions 应用提供单元测试指南。 有关 .NET 隔离辅助角色中的 Durable Functions 的详细信息,请参阅 .NET 隔离辅助角色文章中的 Durable Functions 。
- 本单元测试指南的完整示例代码可在示例代码存储库中找到。
- 有关使用 C# 进程内的 Durable Functions,请参阅 单元测试指南。
本文中的示例需要了解以下概念和框架:
通过以下接口和类支持模拟:
-
DurableTaskClient
- 对于协调器客户端操作 -
TaskOrchestrationContext
- 对于协调器函数执行 -
FunctionContext
- 对于函数执行上下文 -
HttpRequestData
和HttpResponseData
- 适用于 HTTP 触发器函数
这些类可以在 Durable Functions 支持的各种触发器和绑定中使用。 在 Azure Functions 执行期间,函数实例在运行时会使用这些类的具体实现来执行函数代码。 对于单元测试,可以传入这些类的模拟对象来测试业务逻辑。
在本部分中,单元测试验证以下 HTTP 触发器函数的逻辑,以便启动新的业务流程。
[Function("HelloCitiesOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
// Function input comes from the request content.
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(HelloCitiesOrchestration));
// Returns an HTTP 202 response with an instance management payload.
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
单元测试验证响应有效负载中的 HTTP 响应状态代码和实例 ID。 测试模拟 DurableTaskClient
以确保可预测的行为。
首先,我们使用模拟框架(在本例中为 Moq)模拟 DurableTaskClient
和 FunctionContext:
// Mock DurableTaskClient
var durableClientMock = new Mock<DurableTaskClient>("testClient");
var functionContextMock = new Mock<FunctionContext>();
然后 对 ScheduleNewOrchestrationInstanceAsync
方法进行模拟,以返回实例 ID:
var instanceId = Guid.NewGuid().ToString();
// Mock ScheduleNewOrchestrationInstanceAsync method
durableClientMock
.Setup(x => x.ScheduleNewOrchestrationInstanceAsync(
It.IsAny<TaskName>(),
It.IsAny<object>(),
It.IsAny<StartOrchestrationOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(instanceId);
接下来,我们需要模拟 HTTP 请求和响应数据:
// Mock HttpRequestData that sent to the http trigger
var mockRequest = MockHttpRequestAndResponseData();
var responseMock = new Mock<HttpResponseData>(functionContextMock.Object);
responseMock.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.Accepted);
下面是用于模拟 HttpRequestData 的完整辅助方法:
private HttpRequestData MockHttpRequestAndResponseData(HttpHeadersCollection? headers = null)
{
var mockObjectSerializer = new Mock<ObjectSerializer>();
// Setup the SerializeAsync method
mockObjectSerializer.Setup(s => s.SerializeAsync(It.IsAny<Stream>(), It.IsAny<object?>(), It.IsAny<Type>(), It.IsAny<CancellationToken>()))
.Returns<Stream, object?, Type, CancellationToken>(async (stream, value, type, token) =>
{
await System.Text.Json.JsonSerializer.SerializeAsync(stream, value, type, cancellationToken: token);
});
var workerOptions = new WorkerOptions
{
Serializer = mockObjectSerializer.Object
};
var mockOptions = new Mock<IOptions<WorkerOptions>>();
mockOptions.Setup(o => o.Value).Returns(workerOptions);
// Mock the service provider
var mockServiceProvider = new Mock<IServiceProvider>();
// Set up the service provider to return the mock IOptions<WorkerOptions>
mockServiceProvider.Setup(sp => sp.GetService(typeof(IOptions<WorkerOptions>)))
.Returns(mockOptions.Object);
// Set up the service provider to return the mock ObjectSerializer
mockServiceProvider.Setup(sp => sp.GetService(typeof(ObjectSerializer)))
.Returns(mockObjectSerializer.Object);
// Create a mock FunctionContext and assign the service provider
var mockFunctionContext = new Mock<FunctionContext>();
mockFunctionContext.SetupGet(c => c.InstanceServices).Returns(mockServiceProvider.Object);
var mockHttpRequestData = new Mock<HttpRequestData>(mockFunctionContext.Object);
// Set up the URL property
mockHttpRequestData.SetupGet(r => r.Url).Returns(new Uri("https://localhost:7075/orchestrators/HelloCities"));
// If headers are provided, use them, otherwise create a new empty HttpHeadersCollection
headers ??= new HttpHeadersCollection();
// Setup the Headers property to return the empty headers
mockHttpRequestData.SetupGet(r => r.Headers).Returns(headers);
var mockHttpResponseData = new Mock<HttpResponseData>(mockFunctionContext.Object)
{
DefaultValue = DefaultValue.Mock
};
// Enable setting StatusCode and Body as mutable properties
mockHttpResponseData.SetupProperty(r => r.StatusCode, HttpStatusCode.OK);
mockHttpResponseData.SetupProperty(r => r.Body, new MemoryStream());
// Setup CreateResponse to return the configured HttpResponseData mock
mockHttpRequestData.Setup(r => r.CreateResponse())
.Returns(mockHttpResponseData.Object);
return mockHttpRequestData.Object;
}
最后,调用函数并验证结果:
var result = await HelloCitiesOrchestration.HttpStart(mockRequest, durableClientMock.Object, functionContextMock.Object);
// Verify the status code
Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);
// Reset stream position for reading
result.Body.Position = 0;
var serializedResponseBody = await System.Text.Json.JsonSerializer.DeserializeAsync<dynamic>(result.Body);
// Verify the response returned contains the right data
Assert.Equal(instanceId, serializedResponseBody!.GetProperty("Id").GetString());
备注
目前,不支持在单元测试中模拟那些在触发器函数中通过 FunctionContext 创建的记录器。
协调器函数管理多个活动函数的执行。 测试业务流程协调程序:
- 模拟
TaskOrchestrationContext
以控制函数的执行 - 将编排器执行所需的
TaskOrchestrationContext
方法替换为CallActivityAsync
模拟函数 - 使用模拟的上下文直接调用编排器
- 使用断言验证协调器结果
在本部分中,单元测试验证协调程序函数 HelloCities
的性能:
[Function(nameof(HelloCitiesOrchestration))]
public static async Task<List<string>> HelloCities(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
ILogger logger = context.CreateReplaySafeLogger(nameof(HelloCities));
logger.LogInformation("Saying hello.");
var outputs = new List<string>();
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"));
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "London"));
return outputs;
}
单元测试验证业务流程协调程序是否使用预期参数调用正确的活动函数并返回预期结果。 测试模拟 TaskOrchestrationContext
以确保可预测的行为。
首先,我们使用模拟框架(在本例中为 Moq)来模拟 TaskOrchestrationContext
:
// Mock TaskOrchestrationContext and setup logger
var contextMock = new Mock<TaskOrchestrationContext>();
然后我们模拟 CreateReplaySafeLogger
方法以返回我们的测试记录器:
testLogger = new Mock<ILogger>();
contextMock.Setup(x => x.CreateReplaySafeLogger(It.IsAny<string>()))
.Returns(testLogger.Object);
接下来,我们将模拟具有每个城市的特定返回值的活动函数调用:
// Mock the activity function calls
contextMock.Setup(x => x.CallActivityAsync<string>(
It.Is<TaskName>(n => n.Name == nameof(HelloCitiesOrchestration.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(HelloCitiesOrchestration.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(HelloCitiesOrchestration.SayHello)),
It.Is<string>(n => n == "London"),
It.IsAny<TaskOptions>()))
.ReturnsAsync("Hello London!");
然后,我们使用模拟上下文调用协调函数:
var result = await HelloCitiesOrchestration.HelloCities(contextMock.Object);
最后,我们验证编排结果和日志记录行为:
// Verify the orchestration result
Assert.Equal(3, result.Count);
Assert.Equal("Hello Tokyo!", result[0]);
Assert.Equal("Hello Seattle!", result[1]);
Assert.Equal("Hello London!", result[2]);
// Verify logging
testLogger.Verify(
x => x.Log(
It.Is<LogLevel>(l => l == LogLevel.Information),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Saying hello")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
活动函数无需进行任何特定于 Durable 的修改即可测试。 Azure Functions 单元测试概述中找到的指南足以测试这些函数。
在本部分中,单元测试验证活动函数的行为 SayHello
:
[Function(nameof(SayHello))]
public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext)
{
return $"Hello {name}!";
}
单元测试验证输出的格式:
[Fact]
public void SayHello_ReturnsExpectedGreeting()
{
var functionContextMock = new Mock<FunctionContext>();
const string name = "Tokyo";
var result = HelloCitiesOrchestration.SayHello(name, functionContextMock.Object);
// Verify the activity function SayHello returns the right result
Assert.Equal($"Hello {name}!", result);
}
备注
目前,不支持在单元测试中模拟活动函数中通过 FunctionContext 创建的记录器。
- 了解有关 xUnit 的详细信息
- 详细了解 Moq
- 详细了解 Azure Functions 隔离工作者模型