单元测试是现代软件开发实践的重要组成部分。 单元测试验证业务逻辑行为,并防止将来引入未注意到的中断性变更。 Durable Functions 可以轻松增加复杂性,因此引入单元测试有助于避免重大更改。 以下部分介绍如何对三种函数类型(业务流程客户端、业务流程协调程序和活动函数)进行单元测试。
注释
针对 .NET 进程内辅助角色和面向 Durable Functions 2.x 的 Durable Functions 应用(以 C# 编写),本文提供单元测试指南。 有关版本之间差异的详细信息,请参阅 Durable Functions 版本一文。
先决条件
本文中的示例需要了解以下概念和框架:
用于模拟的基类
通过以下接口支持模拟:
这些接口可以与 Durable Functions 支持的各种触发器和绑定一起使用。 在执行 Azure Functions 时,函数运行时会使用这些接口的具体实现来运行函数代码。 对于单元测试,可以传入这些接口的模拟版本来测试业务逻辑。
对触发器函数进行单元测试
在本部分中,单元测试验证以下 HTTP 触发器函数的逻辑,以便启动新的业务流程。
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
namespace VSSample
{
public static class HttpStart
{
[FunctionName("HttpStart")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
[DurableClient] IDurableClient starter,
string functionName,
ILogger log)
{
object eventData = await req.Content.ReadAsAsync<object>();
string instanceId = await starter.StartNewAsync(functionName, eventData);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
}
}
单元测试任务验证响应有效负载中提供的 Retry-After 标头的值。 因此,单元测试模拟某些 IDurableClient 方法,以确保可预测的行为。
首先,我们使用模拟框架(在本例中为 moq )来模拟 IDurableClient:
// Mock IDurableClient
var durableClientMock = new Mock<IDurableClient>();
注释
虽然可以通过直接将接口实现为类来模拟接口,但模拟框架可通过各种方式简化该过程。 例如,如果在次要版本中向接口添加了新方法,moq 不需要任何代码更改,这与具体实现不同。
然后 StartNewAsync 方法被模拟,以返回一个已知的实例 ID。
// Mock StartNewAsync method
durableClientMock.
Setup(x => x.StartNewAsync(functionName, It.IsAny<object>())).
ReturnsAsync(instanceId);
接下来,测试需要处理 CreateCheckStatusResponse。 由于 CreateCheckStatusResponse 是扩展方法,因此无法直接使用 Moq 对其进行模拟。 相反,模拟底层 CreateHttpManagementPayload 方法,这是 IDurableClient 的一个实例方法。
// CreateCheckStatusResponse is an extension method and cannot be mocked directly.
// Mock CreateHttpManagementPayload, which is the underlying instance method.
durableClientMock
.Setup(x => x.CreateHttpManagementPayload(instanceId))
.Returns(new HttpManagementPayload
{
Id = instanceId,
StatusQueryGetUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}",
SendEventPostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/{{eventName}}",
TerminatePostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/terminate",
PurgeHistoryDeleteUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}"
});
注释
CreateCheckStatusResponse 是内部调用 CreateHttpManagementPayload的扩展方法。 扩展方法是静态的,不能使用标准模拟框架(如 Moq)进行模拟。 通过模拟 CreateHttpManagementPayload,可以控制扩展方法使用的数据。
另外还要模拟 ILogger:
// Mock ILogger
var loggerMock = new Mock<ILogger>();
现在,从单元测试调用 Run 方法:
// Call Orchestration trigger function
var result = await HttpStart.Run(
new HttpRequestMessage()
{
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
},
durableClientMock.Object,
functionName,
loggerMock.Object);
最后一步是将输出与预期值进行比较:
// Validate the response status code
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
合并所有这些步骤后,单元测试具有以下代码:
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace VSSample.Tests
{
public class HttpStartTests
{
[Fact]
public async Task HttpStart_returns_management_payload()
{
// Arrange
string instanceId = "7E467BDB-213F-407A-B86A-1954053D3C24";
string functionName = "E1_HelloSequence";
var durableClientMock = new Mock<IDurableClient>();
durableClientMock
.Setup(x => x.StartNewAsync(functionName, It.IsAny<object>()))
.ReturnsAsync(instanceId);
// CreateCheckStatusResponse is an extension method and cannot be mocked directly.
// Mock CreateHttpManagementPayload, which is the underlying instance method.
durableClientMock
.Setup(x => x.CreateHttpManagementPayload(instanceId))
.Returns(new HttpManagementPayload
{
Id = instanceId,
StatusQueryGetUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}",
SendEventPostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/{{eventName}}",
TerminatePostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/terminate",
PurgeHistoryDeleteUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}"
});
var loggerMock = new Mock<ILogger>();
// Act
var result = await HttpStart.Run(
new HttpRequestMessage
{
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
},
durableClientMock.Object,
functionName,
loggerMock.Object);
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}
}
}
对业务流程协调程序函数进行单元测试
业务流程协调程序函数对于单元测试更有趣,因为它们通常有更多的业务逻辑。
在本部分中,单元测试验证 Orchestrator 函数的 E1_HelloSequence 输出:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
namespace VSSample
{
public static class HelloSequence
{
[FunctionName("E1_HelloSequence")]
public static async Task<List<string>> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var outputs = new List<string>();
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "London"));
return outputs;
}
[FunctionName("E1_SayHello")]
public static string SayHello([ActivityTrigger] IDurableActivityContext context)
{
string name = context.GetInput<string>();
return $"Hello {name}!";
}
}
}
单元测试代码首先创建模拟:
var durableOrchestrationContextMock = new Mock<IDurableOrchestrationContext>();
然后模拟活动方法调用:
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "London")).ReturnsAsync("Hello London!");
接下来,单元测试调用 HelloSequence.Run 该方法:
var result = await HelloSequence.Run(durableOrchestrationContextMock.Object);
最后,输出已经被验证:
Assert.Equal(3, result.Count);
Assert.Equal("Hello Tokyo!", result[0]);
Assert.Equal("Hello Seattle!", result[1]);
Assert.Equal("Hello London!", result[2]);
合并上述步骤后,单元测试具有以下代码:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Moq;
using Xunit;
namespace VSSample.Tests
{
public class HelloSequenceOrchestratorTests
{
[Fact]
public async Task Run_returns_multiple_greetings()
{
var durableOrchestrationContextMock = new Mock<IDurableOrchestrationContext>();
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "London")).ReturnsAsync("Hello London!");
var result = await HelloSequence.Run(durableOrchestrationContextMock.Object);
Assert.Equal(3, result.Count);
Assert.Equal("Hello Tokyo!", result[0]);
Assert.Equal("Hello Seattle!", result[1]);
Assert.Equal("Hello London!", result[2]);
}
}
}
对活动函数进行单元测试
活动函数的单元测试方式与非持久函数相同。
在本部分中,单元测试验证活动函数的行为 E1_SayHello :
[FunctionName("E1_SayHello")]
public static string SayHello([ActivityTrigger] IDurableActivityContext context)
{
string name = context.GetInput<string>();
return $"Hello {name}!";
}
单元测试验证输出的格式。 这些单元测试直接使用参数类型或模拟 IDurableActivityContext 类:
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Moq;
using Xunit;
namespace VSSample.Tests
{
public class HelloSequenceActivityTests
{
[Fact]
public void SayHello_returns_greeting()
{
var durableActivityContextMock = new Mock<IDurableActivityContext>();
durableActivityContextMock.Setup(x => x.GetInput<string>()).Returns("World");
var result = HelloSequence.SayHello(durableActivityContextMock.Object);
Assert.Equal("Hello World!", result);
}
}
}