Testing an Azure Function in .NET 5

Viewed 6538

I've started developing Azure Functions and now I want to create my first unit/integration test, but I'm completely stuck. Although I have a very simple Function with an HTTP Trigger and HTTP and Storage Queue output, it seems ridiculously complex te test this.

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 internet to find a good approach, I still didn't find a way to mock HttpRequestData and FunctionContext. 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-dotnet-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? Are there any good articles or examples on how to test an Azure Function in dotnet-isolated?

4 Answers

Solution 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

I added the Logger via Dependency Injection and created my own implementations for HttpRequestData and HttpResponseData. This is way easier to re-use and makes the tests itself cleaner.

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

Now the test looks like this:

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

To complement what you have been doing, I was trying to mock GetLogger() so that I could inject ILogger; unlucky enough, GetLogger() is an extension (static) so it cannot be mocked through reflection. I am now on the way to mocking the fields which are used by the GetLogger() extension (.Net source code).

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. Think about some middleware you have set up or authentication that will not run when calling the Function directly.

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

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. Before that, you can set up your other dependencies which you don't want to mock, such as Azurite and some blob data.

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:

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