在 .NET 5 中测试 Azure 函数
Testing an Azure Function in .NET 5
我已经开始开发 Azure Functions,现在我想创建我的第一个 unit/integration 测试,但我完全卡住了。虽然我有一个带有 HTTP 触发器和 HTTP 和存储队列输出的非常简单的函数,但测试它似乎复杂得可笑。
代码(简体):
public class MyOutput
{
[QueueOutput("my-queue-name", Connection = "my-connection")]
public string QueueMessage { get; set; }
public HttpResponseData HttpResponse { get; set; }
}
public static class MyFunction
{
[Function(nameof(MyFunction))]
public static async Task<MyOutput> Run(
[HttpTrigger(AuthorizationLevel.Function, "POST")] HttpRequestData req,
FunctionContext executionContext)
{
var logger = executionContext.GetLogger(nameof(MyFunction));
logger.LogInformation("Received {Bytes} bytes", req.Body.Length);
//implementation
}
}
现在我希望构建这样的测试:
public async Task Test()
{
var response = await MyFunction.Run(..., ...);
Assert.IsNotNull(response);
}
在互联网上寻找了好几个小时后,我仍然没有找到模拟 HttpRequestData
和 FunctionContext
的方法。我还通过设置服务器来寻找完整的集成测试,但这看起来真的很复杂。我最后得到的唯一结果是:https://github.com/Azure/azure-functions-dotnet-worker/blob/72b9d17a485eda1e6e3626a9472948be1152ab7d/test/E2ETests/E2ETests/HttpEndToEndTests.cs
有没有人有在 .NET 5 中测试 Azure Functions 的经验,谁能给我一个正确的方向?是否有关于如何在 dotnet-isolated 中测试 Azure 函数的好文章或示例?
解决方案 1
我终于可以嘲笑整件事了。绝对不是我最好的作品,可以使用一些重构,但至少我得到了一个工作原型:
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<ILoggerFactory, LoggerFactory>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var context = new Mock<FunctionContext>();
context.SetupProperty(c => c.InstanceServices, serviceProvider);
var byteArray = Encoding.ASCII.GetBytes("test");
var bodyStream = new MemoryStream(byteArray);
var request = new Mock<HttpRequestData>(context.Object);
request.Setup(r => r.Body).Returns(bodyStream);
request.Setup(r => r.CreateResponse()).Returns(() =>
{
var response = new Mock<HttpResponseData>(context.Object);
response.SetupProperty(r => r.Headers, new HttpHeadersCollection());
response.SetupProperty(r => r.StatusCode);
response.SetupProperty(r => r.Body, new MemoryStream());
return response.Object;
});
var result = await MyFunction.Run(request.Object, context.Object);
result.HttpResponse.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(result.HttpResponse.Body);
var responseBody = await reader.ReadToEndAsync();
Assert.IsNotNull(result);
Assert.AreEqual(HttpStatusCode.OK, result.HttpResponse.StatusCode);
Assert.AreEqual("Hello test", responseBody);
解决方案 2
我通过依赖注入添加了 Logger,并为 HttpRequestData 和 HttpResponseData 创建了我自己的实现。这样更容易重用并使测试本身更干净。
public class FakeHttpRequestData : HttpRequestData
{
public FakeHttpRequestData(FunctionContext functionContext, Uri url, Stream body = null) : base(functionContext)
{
Url = url;
Body = body ?? new MemoryStream();
}
public override Stream Body { get; } = new MemoryStream();
public override HttpHeadersCollection Headers { get; } = new HttpHeadersCollection();
public override IReadOnlyCollection<IHttpCookie> Cookies { get; }
public override Uri Url { get; }
public override IEnumerable<ClaimsIdentity> Identities { get; }
public override string Method { get; }
public override HttpResponseData CreateResponse()
{
return new FakeHttpResponseData(FunctionContext);
}
}
public class FakeHttpResponseData : HttpResponseData
{
public FakeHttpResponseData(FunctionContext functionContext) : base(functionContext)
{
}
public override HttpStatusCode StatusCode { get; set; }
public override HttpHeadersCollection Headers { get; set; } = new HttpHeadersCollection();
public override Stream Body { get; set; } = new MemoryStream();
public override HttpCookies Cookies { get; }
}
现在测试看起来像这样:
// Arrange
var body = new MemoryStream(Encoding.ASCII.GetBytes("{ \"test\": true }"))
var context = new Mock<FunctionContext>();
var request = new FakeHttpRequestData(
context.Object,
new Uri("https://whosebug.com"),
body);
// Act
var function = new MyFunction(new NullLogger<MyFunction>());
var result = await function.Run(request);
result.HttpResponse.Body.Position = 0;
// Assert
var reader = new StreamReader(result.HttpResponse.Body);
var responseBody = await reader.ReadToEndAsync();
Assert.IsNotNull(result);
Assert.AreEqual(HttpStatusCode.OK, result.HttpResponse.StatusCode);
Assert.AreEqual("Hello test", responseBody);
调用函数的 运行 方法与其说是集成测试,不如说是一个单元。想想你设置的一些中间件或认证,在直接调用Function时不会运行。
但是,目前看来,还没有优雅的进程内方法来对基于 .NET 5 的 Azure Isolated Functions 进行集成测试。请参阅:https://github.com/Azure/azure-functions-dotnet-worker/issues/541 and https://github.com/Azure/azure-functions-dotnet-worker/issues/281
我通过在本地启动 Azure 函数主机“func”、托管我的函数然后使用测试本身的普通 HttpClient 取得了一些成功。在此之前,您可以设置您不想模拟的其他依赖项,例如 Azurite 和一些 blob 数据。
为了补充您一直在做的事情,我试图模拟 GetLogger()
以便我可以注入 ILogger;不幸的是,GetLogger()
是一个扩展(静态的)所以它不能通过反射来模拟。我现在正在模拟 GetLogger()
扩展(.Net source code)使用的字段。
看起来像这样:
using Mock;
using Microsoft.Extensions.DependencyInjection;
public static FunctionContext CreateFunctionContext(ILogger logger = null)
{
logger = logger ?? CreateNullLogger();
var LoggerFactory = new Mock<ILoggerFactory>();
LoggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(logger);
var InstanceServices = new Mock<IServiceProvider>();
InstanceServices.Setup(p => p.GetService(It.IsAny<Type>())).Returns(LoggerFactory.Object);
var context = new Mock<FunctionContext>();
context.Setup(p => p.InstanceServices).Returns(InstanceServices.Object);
return context.Object;
}
您可以模拟上下文并仅提供您将从中使用的内容。在这里,您有一种使用 NSubstitute 获取 ILogger 的方法,您可以根据需要进行自定义:
[TestClass]
public class FunctionTest
{
private static readonly FunctionContext _context = Substitute.For<FunctionContext>();
private static readonly ILogger _logger = Substitute.For<ILogger>();
[ClassInitialize]
public static void ClassSetupAsync(TestContext _)
{
// create a mock log factory that returns a mocked logger
var logFactory = Substitute.For<ILoggerFactory>();
logFactory
.CreateLogger(Arg.Any<string>())
.Returns(_logger);
// create a mock service provider that knows only about logs
var services = Substitute.For<IServiceProvider>();
services
.GetService(Arg.Any<Type>())
.Returns(logFactory);
// use the mocked service provider in the mocked context
// you can pass this context to your Azure Function
_context
.InstanceServices
.Returns(services);
}
}
我已经开始开发 Azure Functions,现在我想创建我的第一个 unit/integration 测试,但我完全卡住了。虽然我有一个带有 HTTP 触发器和 HTTP 和存储队列输出的非常简单的函数,但测试它似乎复杂得可笑。
代码(简体):
public class MyOutput
{
[QueueOutput("my-queue-name", Connection = "my-connection")]
public string QueueMessage { get; set; }
public HttpResponseData HttpResponse { get; set; }
}
public static class MyFunction
{
[Function(nameof(MyFunction))]
public static async Task<MyOutput> Run(
[HttpTrigger(AuthorizationLevel.Function, "POST")] HttpRequestData req,
FunctionContext executionContext)
{
var logger = executionContext.GetLogger(nameof(MyFunction));
logger.LogInformation("Received {Bytes} bytes", req.Body.Length);
//implementation
}
}
现在我希望构建这样的测试:
public async Task Test()
{
var response = await MyFunction.Run(..., ...);
Assert.IsNotNull(response);
}
在互联网上寻找了好几个小时后,我仍然没有找到模拟 HttpRequestData
和 FunctionContext
的方法。我还通过设置服务器来寻找完整的集成测试,但这看起来真的很复杂。我最后得到的唯一结果是:https://github.com/Azure/azure-functions-dotnet-worker/blob/72b9d17a485eda1e6e3626a9472948be1152ab7d/test/E2ETests/E2ETests/HttpEndToEndTests.cs
有没有人有在 .NET 5 中测试 Azure Functions 的经验,谁能给我一个正确的方向?是否有关于如何在 dotnet-isolated 中测试 Azure 函数的好文章或示例?
解决方案 1
我终于可以嘲笑整件事了。绝对不是我最好的作品,可以使用一些重构,但至少我得到了一个工作原型:
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<ILoggerFactory, LoggerFactory>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var context = new Mock<FunctionContext>();
context.SetupProperty(c => c.InstanceServices, serviceProvider);
var byteArray = Encoding.ASCII.GetBytes("test");
var bodyStream = new MemoryStream(byteArray);
var request = new Mock<HttpRequestData>(context.Object);
request.Setup(r => r.Body).Returns(bodyStream);
request.Setup(r => r.CreateResponse()).Returns(() =>
{
var response = new Mock<HttpResponseData>(context.Object);
response.SetupProperty(r => r.Headers, new HttpHeadersCollection());
response.SetupProperty(r => r.StatusCode);
response.SetupProperty(r => r.Body, new MemoryStream());
return response.Object;
});
var result = await MyFunction.Run(request.Object, context.Object);
result.HttpResponse.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(result.HttpResponse.Body);
var responseBody = await reader.ReadToEndAsync();
Assert.IsNotNull(result);
Assert.AreEqual(HttpStatusCode.OK, result.HttpResponse.StatusCode);
Assert.AreEqual("Hello test", responseBody);
解决方案 2
我通过依赖注入添加了 Logger,并为 HttpRequestData 和 HttpResponseData 创建了我自己的实现。这样更容易重用并使测试本身更干净。
public class FakeHttpRequestData : HttpRequestData
{
public FakeHttpRequestData(FunctionContext functionContext, Uri url, Stream body = null) : base(functionContext)
{
Url = url;
Body = body ?? new MemoryStream();
}
public override Stream Body { get; } = new MemoryStream();
public override HttpHeadersCollection Headers { get; } = new HttpHeadersCollection();
public override IReadOnlyCollection<IHttpCookie> Cookies { get; }
public override Uri Url { get; }
public override IEnumerable<ClaimsIdentity> Identities { get; }
public override string Method { get; }
public override HttpResponseData CreateResponse()
{
return new FakeHttpResponseData(FunctionContext);
}
}
public class FakeHttpResponseData : HttpResponseData
{
public FakeHttpResponseData(FunctionContext functionContext) : base(functionContext)
{
}
public override HttpStatusCode StatusCode { get; set; }
public override HttpHeadersCollection Headers { get; set; } = new HttpHeadersCollection();
public override Stream Body { get; set; } = new MemoryStream();
public override HttpCookies Cookies { get; }
}
现在测试看起来像这样:
// Arrange
var body = new MemoryStream(Encoding.ASCII.GetBytes("{ \"test\": true }"))
var context = new Mock<FunctionContext>();
var request = new FakeHttpRequestData(
context.Object,
new Uri("https://whosebug.com"),
body);
// Act
var function = new MyFunction(new NullLogger<MyFunction>());
var result = await function.Run(request);
result.HttpResponse.Body.Position = 0;
// Assert
var reader = new StreamReader(result.HttpResponse.Body);
var responseBody = await reader.ReadToEndAsync();
Assert.IsNotNull(result);
Assert.AreEqual(HttpStatusCode.OK, result.HttpResponse.StatusCode);
Assert.AreEqual("Hello test", responseBody);
调用函数的 运行 方法与其说是集成测试,不如说是一个单元。想想你设置的一些中间件或认证,在直接调用Function时不会运行。
但是,目前看来,还没有优雅的进程内方法来对基于 .NET 5 的 Azure Isolated Functions 进行集成测试。请参阅:https://github.com/Azure/azure-functions-dotnet-worker/issues/541 and https://github.com/Azure/azure-functions-dotnet-worker/issues/281
我通过在本地启动 Azure 函数主机“func”、托管我的函数然后使用测试本身的普通 HttpClient 取得了一些成功。在此之前,您可以设置您不想模拟的其他依赖项,例如 Azurite 和一些 blob 数据。
为了补充您一直在做的事情,我试图模拟 GetLogger()
以便我可以注入 ILogger;不幸的是,GetLogger()
是一个扩展(静态的)所以它不能通过反射来模拟。我现在正在模拟 GetLogger()
扩展(.Net source code)使用的字段。
看起来像这样:
using Mock;
using Microsoft.Extensions.DependencyInjection;
public static FunctionContext CreateFunctionContext(ILogger logger = null)
{
logger = logger ?? CreateNullLogger();
var LoggerFactory = new Mock<ILoggerFactory>();
LoggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(logger);
var InstanceServices = new Mock<IServiceProvider>();
InstanceServices.Setup(p => p.GetService(It.IsAny<Type>())).Returns(LoggerFactory.Object);
var context = new Mock<FunctionContext>();
context.Setup(p => p.InstanceServices).Returns(InstanceServices.Object);
return context.Object;
}
您可以模拟上下文并仅提供您将从中使用的内容。在这里,您有一种使用 NSubstitute 获取 ILogger 的方法,您可以根据需要进行自定义:
[TestClass]
public class FunctionTest
{
private static readonly FunctionContext _context = Substitute.For<FunctionContext>();
private static readonly ILogger _logger = Substitute.For<ILogger>();
[ClassInitialize]
public static void ClassSetupAsync(TestContext _)
{
// create a mock log factory that returns a mocked logger
var logFactory = Substitute.For<ILoggerFactory>();
logFactory
.CreateLogger(Arg.Any<string>())
.Returns(_logger);
// create a mock service provider that knows only about logs
var services = Substitute.For<IServiceProvider>();
services
.GetService(Arg.Any<Type>())
.Returns(logFactory);
// use the mocked service provider in the mocked context
// you can pass this context to your Azure Function
_context
.InstanceServices
.Returns(services);
}
}