.netazureazure-functions

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. 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?


Solution

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