在.NET 5中测试Azure函数

qmb5sa22  于 2023-06-06  发布在  .NET
关注(0)|答案(5)|浏览(204)

我已经开始开发Azure Functions,现在我想创建我的第一个单元/集成测试,但我完全卡住了。虽然我有一个非常简单的函数,带有一个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);
}

我在互联网上找了几个小时来寻找一个好的方法,但仍然没有找到模拟HttpRequestDataFunctionContext的方法。我还通过设置服务器来寻找完整的集成测试,但这似乎真的很复杂。我唯一的结局是:https://github.com/Azure/azure-functions-dotnet-worker/blob/72b9d17a485eda1e6e3626a9472948be1152ab7d/test/E2ETests/E2ETests/HttpEndToEndTests.cs
有没有人有在.NET 5中测试Azure Functions的经验,谁能给予我一个正确的方向?有没有关于如何在dotnet-isolated中测试Azure函数的好文章或示例?

bvjveswy

bvjveswy1#

方案一

我终于可以嘲笑整件事了。肯定不是我最好的作品,可以使用一些重构,但至少我得到了一个工作原型:

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);

方案二

我通过依赖注入添加了Logger,并为 HttpRequestDataHttpResponseData 创建了自己的实现。这更容易重用,并使测试本身更干净。

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://stackoverflow.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);
igetnqfo

igetnqfo2#

为了补充您所做的工作,我尝试模拟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;

    }
afdcj2ne

afdcj2ne3#

调用Function的Run方法与其说是集成测试,不如说是一个单元。考虑一下您设置的一些中间件或身份验证,它们在直接调用Function时不会运行。
然而,目前似乎没有优雅的进程内方法来使用基于.NET 5的Azure隔离函数进行集成测试。参见:https://github.com/Azure/azure-functions-dotnet-worker/issues/541https://github.com/Azure/azure-functions-dotnet-worker/issues/281
我通过在本地启动Azure函数主机“func”,托管我的函数,然后从测试本身使用普通的HttpClient,取得了一些成功。在此之前,您可以设置您不想模拟的其他依赖项,例如Azurite和一些blob数据。

noj0wjuj

noj0wjuj4#

您可以模拟上下文,并只提供您将从中使用的内容。这里有一种使用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);
    }
}
webghufk

webghufk5#

值得一提的是,这是我使用.NET 7和Azure Functions v4的实现。它扩展了@VincentBitter的工作,是一个可行的,多请求能力的HttpClientFactory模拟。
这里要注意的主要事情是添加了
System. out. println();*

单元测试模拟设置

MockHttpClientFactory

public class MockHttpClientFactory
{
    public static IHttpClientFactory Create(string name, MockHttpResponse response)
    {
        return Create(name, new List<MockHttpResponse> { response });
    }

    public static IHttpClientFactory Create(string name, List<MockHttpResponse> responses)
    {
                    
        Mock<HttpMessageHandler> messageHandler = SendAsyncHandler(responses);

        var mockHttpClientFactory = new Mock<IHttpClientFactory>();

        mockHttpClientFactory
            .Setup(x => x.CreateClient(name))
            .Returns(new HttpClient(messageHandler.Object)
            {
                BaseAddress = new Uri("https://mockdomain.mock")
            });

        return mockHttpClientFactory.Object;
    }

    private static Mock<HttpMessageHandler> SendAsyncHandler(List<MockHttpResponse> responses)
    {
        var messageHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);

        foreach(var response in responses)
        {
            messageHandler
                .Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync",
                    ItExpr.Is<HttpRequestMessage>(r => r.RequestUri!.PathAndQuery == response.UrlPart),
                    ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = response.StatusCode,
                    Content = (response.Response?.GetType() == typeof(string)
                        ? new StringContent(response.Response?.ToString() ?? "")
                        : new StringContent(JsonSerializer.Serialize(response.Response)))
                })
                .Verifiable();
        }               

        return messageHandler;
    }
}

MockHttpResponse

public class MockHttpResponse
{
    public MockHttpResponse()
    {           
    }

    public MockHttpResponse(string urlPart, object response, HttpStatusCode statusCode)
    {
        this.UrlPart = urlPart;
        this.Response = response;
        this.StatusCode = statusCode;
    }

    public string UrlPart { get; set; } = String.Empty;

    public object Response { get; set; } = default!;

    public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
}

MockHttpRequestData

public class MockHttpRequestData
{ 
    public static HttpRequestData Create()
    {
        return Create<string>("");
    }   
    

    public static HttpRequestData Create<T>(T requestData) where T : class
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddFunctionsWorkerDefaults();

        var serializedData = JsonSerializer.Serialize(requestData);
        var bodyDataStream = new MemoryStream(Encoding.UTF8.GetBytes(serializedData));

        var context = new Mock<FunctionContext>();
        context.SetupProperty(context => context.InstanceServices, serviceCollection.BuildServiceProvider());

        var request = new Mock<HttpRequestData>(context.Object);
        request.Setup(r => r.Body).Returns(bodyDataStream);
        request.Setup(r => r.CreateResponse()).Returns(new MockHttpResponseData(context.Object));

        return request.Object;
    }
}

MockHttpResponseData

public class MockHttpResponseData : HttpResponseData
{
    public MockHttpResponseData(FunctionContext functionContext) : base(functionContext)
    {           
    }
    

    public override HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;

    public override HttpHeadersCollection Headers { get; set; } = new HttpHeadersCollection();

    public override Stream Body { get; set; } = new MemoryStream();

    public override HttpCookies Cookies { get; }
}

用法

Azure函数方法

这个azure函数是用DI设置的,它使用了一个HttpClient对象。细节超出了本文的范围。您可以Google了解更多信息。

public class Function1
{
    private readonly HttpClient httpClient;

    public Function1(IHttpClientFactory httpClientFactory)
    {
        this.httpClient = httpClientFactory.CreateClient("WhateverYouNamedIt");
    }


    [Function("Function1")]
    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
    {
        var httpResponse = await this.httpClient.GetAsync("/some-path");
        var httpResponseContent = await httpResponse.Content.ReadAsStringAsync();

        // do something with the httpResponse or Content

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteStringAsync(httpResponseContent);
        
        return response;
    }               
}

简单用例

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", new MockHttpResponse());

        var exception = Record.Exception(() => new Function1(httpClientFactory));

        Assert.Null(exception);
    }
}

更真实的用例

[Fact]
    public async Task Test2()
    {
        var httpResponses = new List<MockHttpResponse>
        {
            new MockHttpResponse
            {
                UrlPart = "/some-path",
                Response = new { Name = "data" }
            }
        };

        var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", httpResponses);
        var httpRequestData = MockHttpRequestData.Create();

        var function1 = new Function1(httpClientFactory);
        var function1Response = await function1.Run(httpRequestData);
        function1Response.Body.Position = 0;

        using var streamReader = new StreamReader(function1Response.Body);
        var function1ResponseBody = await streamReader.ReadToEndAsync();
                
        Assert.Equal("{\"Name\":\"data\"}", function1ResponseBody);
    }

相关问题