简体   繁体   English

在 .NET 中测试 Azure Function 5

[英]Testing an Azure Function in .NET 5

I've started developing Azure Functions and now I want to create my first unit/integration test, but I'm completely stuck.我已经开始开发 Azure 函数,现在我想创建我的第一个单元/集成测试,但我完全卡住了。 Although I have a very simple Function with an HTTP Trigger and HTTP and Storage Queue output, it seems ridiculously complex te test this.虽然我有一个非常简单的 Function 和 HTTP 触发器和 HTTP 和存储队列 output,但测试这个似乎复杂得可笑。

The code (simplified):代码(简化):

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
    }
}

Now I'd expect to build a test like this:现在我希望构建这样的测试:

public async Task Test()
{
    var response = await MyFunction.Run(..., ...);
    Assert.IsNotNull(response);
}

After looking hours on the inte.net to find a good approach, I still didn't find a way to mock HttpRequestData and FunctionContext .在 inte.net 上寻找好几个小时后,我仍然没有找到模拟HttpRequestDataFunctionContext方法。 I also looked for a full integration test by setting up a server, but this seems really complex.我还通过设置服务器来寻找完整的集成测试,但这看起来真的很复杂。 The only thing I ended up was this: https://github.com/Azure/azure-functions-do.net-worker/blob/72b9d17a485eda1e6e3626a9472948be1152ab7d/test/E2ETests/E2ETests/HttpEndToEndTests.cs我最终得到的唯一结果是: https://github.com/Azure/azure-functions-do.net-worker/blob/72b9d17a485eda1e6e3626a9472948be1152ab7d/test/E2ETests/E2ETests/HttpEndToEndTests.cs

Does anyone have experience testing Azure Functions in .NET 5, who can give me a push in the right direction?有没有人有测试 Azure 功能的经验 .NET 5,谁能给我一个正确方向的推动? Are there any good articles or examples on how to test an Azure Function in do.net-isolated?是否有关于如何在 do.net-isolated 中测试 Azure Function 的好文章或示例?

Solution 1解决方案 1

I was finally able to mock the whole thing.我终于能够嘲笑整个事情了。 Definitely not my best work and can use some refactoring, but at least I got a working prototype:绝对不是我最好的工作,可以使用一些重构,但至少我得到了一个工作原型:

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

Solution 2解决方案 2

I added the Logger via Dependency Injection and created my own implementations for HttpRequestData and HttpResponseData .我通过依赖注入添加了 Logger,并为HttpRequestDataHttpResponseData创建了我自己的实现。 This is way easier to re-use and makes the tests itself cleaner.这更容易重用,并使测试本身更干净。

public class FakeHttpRequestData : HttpRequestData
{
    public FakeHttpRequestData(FunctionContext functionContext) : base(functionContext)
    {
    }

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

Now the test looks like this:现在测试看起来像这样:

// Arrange
var context = new Mock<FunctionContext>();
var request = new FakeHttpRequestData(context.Object);
await request.Body.WriteAsync(Encoding.ASCII.GetBytes("test"));
request.Body.Position = 0;

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

To complement what you have been doing, I was trying to mock GetLogger() so that I could inject ILogger;为了补充你一直在做的事情,我试图模拟GetLogger()以便我可以注入 ILogger; unlucky enough, GetLogger() is an extension (static) so it cannot be mocked through reflection.不幸的是, GetLogger()是一个扩展(静态),因此不能通过反射来模拟它。 I am now on the way to mocking the fields which are used by the GetLogger() extension ( .Net source code ).我现在正在前往 mocking GetLogger()扩展( .Net 源代码)使用的字段。

That would look like this:看起来像这样:

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;

    }

Calling the Run method of your Function is more a unit than an integration test.调用 Function 的 Run 方法与其说是集成测试,不如说是一个单元。 Think about some middleware you have set up or authentication that will not run when calling the Function directly.想想您设置的一些中间件或直接调用 Function 时不会运行的身份验证。

However, it seems, currently, there is no elegant, in-process way to do integration testing with Azure Isolated Functions based on .NET 5. Refer to: https://github.com/Azure/azure-functions-dotnet-worker/issues/541 and https://github.com/Azure/azure-functions-dotnet-worker/issues/281但是,目前似乎没有优雅的进程内方法来使用基于 .NET 5 的 Azure 隔离函数进行集成测试。请参阅: https:问题/541https://github.com/Azure/azure-functions-dotnet-worker/issues/281

I had some success by starting the Azure Function Host "func" locally, hosting my functions and then using a normal HttpClient from the Test itself.通过在本地启动 Azure Function 主机“func”,托管我的函数,然后使用测试本身的普通 HttpClient,我取得了一些成功。 Before that, you can set up your other dependencies which you don't want to mock, such as Azurite and some blob data.在此之前,您可以设置您不想模拟的其他依赖项,例如 Azurite 和一些 blob 数据。

You can mock the context and provide only what you will use from it.您可以模拟上下文并仅提供您将使用的内容。 Here you have an approach using NSubstitute to obtain an ILogger you can customize as you need:在这里,您有一种使用 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);
    }
}

For what it's worth, this is my implementation using .NET 7 and Azure Functions v4.对于它的价值,这是我使用 .NET 7 和 Azure 函数 v4 的实现。 It expands on the work of @VincentBitter and is a workable, multi-request capable HttpClientFactory mock.它扩展了@VincentBitter 的工作,是一个可行的、支持多请求的 HttpClientFactory 模拟。

The main thing to note here is the addition of the这里要注意的主要事情是添加了

serviceCollection.AddFunctionsWorkerDefaults(); serviceCollection.AddFunctionsWorkerDefaults();

Unit Test Mock Setup单元测试模拟设置


MockHttpClientFactory 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 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 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 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; }
}

Usage用法


The Azure Function Method Azure Function 方法

This azure function has been setup with DI and uses an HttpClient object. Details are out of scope for this post.此 azure function 已使用 DI 设置并使用 HttpClient object。此帖子的详细信息来自 scope。 You can Google for more info.您可以谷歌获取更多信息。

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

Simple Use Case简单用例

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

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

        Assert.Null(exception);
    }
}

More Realistic Use Case更现实的用例

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM