Azure / azure-functions-dotnet-worker

Azure Functions out-of-process .NET language worker
MIT License
416 stars 181 forks source link

how to mock HttpRequestData sung Moq for testing Azure Function Isolated .Net 8 #2263

Open dioum2touba opened 7 months ago

dioum2touba commented 7 months ago

Description

For using Moq, I share with you how to implement this solution using Moq to test Azure Function Isolated .Net 8

public static HttpRequestData CreateMockHttpRequestData(string body, string? schema = null)
{
    var functionContext = new Mock<FunctionContext>();
    var requestData = new Mock<HttpRequestData>(functionContext.Object);
    var serviceCollection = new ServiceCollection();
    serviceCollection.AddSingleton(Options.Create(new WorkerOptions { Serializer = new JsonObjectSerializer() }));

    var serviceProvider = serviceCollection.BuildServiceProvider();
    functionContext.Setup(context => context.InstanceServices).Returns(serviceProvider);

    var bodyForHttpRequest = GetBodyForHttpRequest(body);
    requestData.Setup(context => context.Body).Returns(bodyForHttpRequest);

    var headersForHttpRequestData = new HttpHeadersCollection();
    if (!string.IsNullOrWhiteSpace(schema))
    {
        headersForHttpRequestData.Add("Authorization", $"{schema} edd2545es.ez5ez5454e.ezdsdsds");
    }

    requestData.Setup(context => context.Headers).Returns(headersForHttpRequestData);

    return requestData.Object;
}

private static MemoryStream GetBodyForHttpRequest(string body)
{
    var byteArray = Encoding.UTF8.GetBytes(body);
    var memoryStream = new MemoryStream(byteArray);
    memoryStream.Flush();
    memoryStream.Position = 0;

    return memoryStream;
}
davidpetric commented 7 months ago

I have built myself a builder for this scenario:


public sealed class MockHttpRequestData : HttpRequestData
{
    private readonly FunctionContext context;

    public MockHttpRequestData(
        FunctionContext context,
        string body)
            : base(context)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(body);
        Body = new MemoryStream(bytes);
        this.context = context;

        Cookies = new List<IHttpCookie>().AsReadOnly();

        Identities = new List<ClaimsIdentity>();
    }

    public override Stream Body { get; }

    public override HttpHeadersCollection Headers { get; } = [];

    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()
    {
        MockHttpResponseData response = new(this.context);

        return response;
    }

    public void AddHeaderKeyVal(string key, string value)
    {
        Headers.Add(key, value);
    }

    public void AddQuery(string key, string value)
    {
        Query.Add(key, value);
    }
}

public sealed class MockHttpResponseData(FunctionContext context) : HttpResponseData(context)
{
    public override HttpStatusCode StatusCode { get; set; }

    public override HttpHeadersCollection Headers { get; set; } = [];

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

    public override HttpCookies Cookies { get; }
}

public class MockHttpRequestDataBuilder
{
    private readonly IServiceCollection requestServiceCollection;
    private IInvocationFeatures invocationFeatures;
    private IServiceProvider requestContextInstanceServices;
    private FunctionContext functionContext;

    private string rawJsonBody;

    public MockHttpRequestDataBuilder()
    {
        this.requestServiceCollection = new ServiceCollection();
        this.requestServiceCollection.AddOptions();
    }

    public MockHttpRequestDataBuilder WithDefaultJsonSerializer()
    {
        this.requestServiceCollection
            .Configure<WorkerOptions>(workerOptions =>
            {
                workerOptions.Serializer =
                    new JsonObjectSerializer(
                        new JsonSerializerOptions
                        {
                            AllowTrailingCommas = true,
                        });
            });

        return this;
    }

    public MockHttpRequestDataBuilder WithCustomJsonSerializerSettings(
        Func<JsonObjectSerializer> jsonObjectSerializerOptions)
    {
        this.requestServiceCollection.Configure<WorkerOptions>(
            workerOptions => workerOptions.Serializer = jsonObjectSerializerOptions());
        return this;
    }

    public MockHttpRequestDataBuilder WithRequestContextInstanceServices(
        IServiceProvider requestContextInstanceServices)
    {
        this.requestContextInstanceServices = requestContextInstanceServices;
        return this;
    }

    public MockHttpRequestDataBuilder WithInvocationFeatures(
        IInvocationFeatures invocationFeatures)
    {
        this.invocationFeatures = invocationFeatures;
        return this;
    }

    public MockHttpRequestDataBuilder WithFakeFunctionContext()
    {
        this.requestContextInstanceServices ??= this.requestServiceCollection.BuildServiceProvider();

        this.functionContext =
            new FakeFunctionContext(this.invocationFeatures)
            {
                InstanceServices = this.requestContextInstanceServices,
            };

        return this;
    }

    public MockHttpRequestDataBuilder WithRawJsonBody(string rawJsonBody)
    {
        this.rawJsonBody = rawJsonBody;
        return this;
    }

    public MockHttpRequestData Build()
        => new(this.functionContext, this.rawJsonBody);
}

Usage:

        MockHttpRequestData mockHttpRequest =
            new MockHttpRequestDataBuilder()
                .WithDefaultJsonSerializer()
                .WithFakeFunctionContext()
                .WithRawJsonBody(rawJsonBody)
                .Build();

        InvoicesFunction invoicesFunction = new(
             invoicesService.Object,
             createInvoiceRequestValidator.Object,
             Mock.Of<ILogger<InvoicesFunction>>());

        HttpResponseData response = await invoicesFunction.CreateInvoiceAsync(mockHttpRequest, id, CancellationToken.None);

        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
udlose commented 7 months ago

@davidpetric , @dioum2touba how do you mock the InvocationFeatures on the FunctionContext?

davidpetric commented 7 months ago

@davidpetric , @dioum2touba how do you mock the InvocationFeatures on the FunctionContext?

This is how I did it for authorization Middleware(this is an older test where I did not have the builder mentioned in the comment above).

[Theory]
[InlineAutoMoqData("http://localhost:7268/api/swagger/ui")]
[InlineAutoMoqData("http://localhost:7268/api/swagger.json")]
[InlineAutoMoqData("http://localhost:7268/api/openapi/v3.json")]
public async Task Invoke_SkipAuthorizationIfSwaggerUrl(string url)
{
    // Arrange
    HostBuilder hostBuilderTest = new();
    hostBuilderTest.ConfigureFunctionsWebApplication(builder => builder.UseMiddleware<AuthorizationMiddleware>());

    bool delegateCalled = false;
    Task MockedFunctionExecutionDelegate(FunctionContext context)
    {
        delegateCalled = true;
        return Task.CompletedTask;
    }

    FunctionExecutionDelegate functionExecutionDelegate = MockedFunctionExecutionDelegate;

    Mock<IHttpRequestDataFeature> httpRequestDataFeature = new();
    Mock<IInvocationFeatures> invocationFeatures = new();

    await using MemoryStream memoryStream = new(Encoding.UTF8.GetBytes(RequestBodyRaw));

    FakeFunctionContext fakeFunctionContext = new(invocationFeatures.Object);

    TestHttpRequestData httpRequestData = new(fakeFunctionContext, memoryStream, RequestMethod, url);

    httpRequestDataFeature
        .Setup(x => x.GetHttpRequestDataAsync(It.IsAny<FunctionContext>()))
        .ReturnsAsync(httpRequestData);

    invocationFeatures.Setup(x => x.Get<IHttpRequestDataFeature>())
        .Returns(httpRequestDataFeature.Object);

    // Act
    AuthorizationMiddleware middleware = this.host.Services.GetService<AuthorizationMiddleware>();

    // Assert
    middleware.Should().NotBeNull();

    await middleware.Invoke(fakeFunctionContext, functionExecutionDelegate);

    delegateCalled.Should().BeTrue();
}
udlose commented 7 months ago

@davidpetric do you have the FakeFunctionContext class? I'm curious how you wire up the InvocationFeatures in the ctor.

davidpetric commented 6 months ago

@davidpetric do you have the FakeFunctionContext class? I'm curious how you wire up the InvocationFeatures in the ctor.

@udlose please see the below code:

public class FakeFunctionContext(IInvocationFeatures features, IDictionary<object, object> items = null) : FunctionContext
{
    public override string InvocationId { get; }

    public override string FunctionId { get; }

    public override TraceContext TraceContext { get; }

    public override BindingContext BindingContext { get; }

    public override RetryContext RetryContext { get; }

    public override IServiceProvider InstanceServices { get; set; }

    public override FunctionDefinition FunctionDefinition { get; }

    public override IDictionary<object, object> Items { get; set; } = items;

    public override IInvocationFeatures Features { get; } = features;
}
satvu commented 6 months ago

Flagging this as potential candidate for a new sample

Joseph-Steven-S commented 3 weeks ago

@davidpetric , @dioum2touba how do you mock the InvocationFeatures on the FunctionContext?

This is how I did it for authorization Middleware(this is an older test where I did not have the builder mentioned in the comment above).

[Theory]
[InlineAutoMoqData("http://localhost:7268/api/swagger/ui")]
[InlineAutoMoqData("http://localhost:7268/api/swagger.json")]
[InlineAutoMoqData("http://localhost:7268/api/openapi/v3.json")]
public async Task Invoke_SkipAuthorizationIfSwaggerUrl(string url)
{
    // Arrange
    HostBuilder hostBuilderTest = new();
    hostBuilderTest.ConfigureFunctionsWebApplication(builder => builder.UseMiddleware<AuthorizationMiddleware>());

    bool delegateCalled = false;
    Task MockedFunctionExecutionDelegate(FunctionContext context)
    {
        delegateCalled = true;
        return Task.CompletedTask;
    }

    FunctionExecutionDelegate functionExecutionDelegate = MockedFunctionExecutionDelegate;

    Mock<IHttpRequestDataFeature> httpRequestDataFeature = new();
    Mock<IInvocationFeatures> invocationFeatures = new();

    await using MemoryStream memoryStream = new(Encoding.UTF8.GetBytes(RequestBodyRaw));

    FakeFunctionContext fakeFunctionContext = new(invocationFeatures.Object);

    TestHttpRequestData httpRequestData = new(fakeFunctionContext, memoryStream, RequestMethod, url);

    httpRequestDataFeature
        .Setup(x => x.GetHttpRequestDataAsync(It.IsAny<FunctionContext>()))
        .ReturnsAsync(httpRequestData);

    invocationFeatures.Setup(x => x.Get<IHttpRequestDataFeature>())
        .Returns(httpRequestDataFeature.Object);

    // Act
    AuthorizationMiddleware middleware = this.host.Services.GetService<AuthorizationMiddleware>();

    // Assert
    middleware.Should().NotBeNull();

    await middleware.Invoke(fakeFunctionContext, functionExecutionDelegate);

    delegateCalled.Should().BeTrue();
}

Would the middleware functionExecutionDelegate need to be configured differently if I was to want to execute it as if a function is actually being called?

Benjlet commented 1 week ago

In case this is of use to others, I have created a basic HttpRequestData builder for .NET 8:

This is based on a few similar issues and respositories, fulfilling some of the simpler use cases for integration/Moq testing with a few examples for basic use cases.

Anyone is welcome to use/adapt/copy whatever areas may help you.

@davidpetric I really liked your fluent builder which inspired the main format, this is the latest thread I found on the topic so hopefully your more advanced Mock middleware example and this collation helps anyone searching