耐久函数单元测试(C# 独立环境)

单元测试是现代软件开发实践的重要组成部分。 单元测试验证业务逻辑行为,并防止将来引入未注意到的中断性变更。 Durable Functions 可以轻松增加复杂性,因此引入单元测试有助于避免重大更改。 以下部分介绍如何对三种函数类型(业务流程客户端、业务流程协调程序和活动函数)进行单元测试。

备注

先决条件

本文中的示例需要了解以下概念和框架:

  • 单元测试
  • Durable Functions
  • xUnit - 测试框架
  • moq - 模拟框架

用于模拟的基类

通过以下接口和类支持模拟:

  • DurableTaskClient - 对于协调器客户端操作
  • TaskOrchestrationContext - 对于协调器函数执行
  • FunctionContext - 对于函数执行上下文
  • HttpRequestDataHttpResponseData - 适用于 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 创建的记录器。

后续步骤