0

I am trying to adapt the Microsoft example for AF 3.0/.NET Core 3.1/xUnit (see Strategies for testing your code in Azure Functions) to work with AF 3.0/.NET 5.0/xUnit. However, I am running into compilation issues.

The Azure Function is a simple HTTP Trigger (GET only), ExportFuncApp.csproj file is as follows:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.12" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.2.0" OutputItemType="Analyzer" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.5.2" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

The ExportFunc.cs file is as follows:

using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

namespace ExportFuncApp
{
    public class ExportFunc
    {
        [Function(nameof(ExportFunc))]
        public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req,
            FunctionContext executionContext)
        {
            var logger = executionContext.GetLogger("ExportFunc");
            logger.LogInformation("C# HTTP trigger function processed a request.");

            var response = req.CreateResponse(HttpStatusCode.OK);
            response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

            response.WriteString("Welcome to Azure Functions!");

            return response;
        }
    }
}

Nothing special there. However, XUnit tests provided by Microsoft (.NET Core 3.1) are not really applicable to .NET 5.0. There was a StackOverflow article on the subject: Testing an Azure Function in .NET 5. 4 solutions were given in the article and all of them have compile issues. The first solution given was (ExportFuncUnitTests2.cs):

using Xunit;
using ExportFuncApp;
using System.Threading.Tasks;
using System.IO;
using System.Text;
using Moq;
using Microsoft.Azure.Functions.Worker;
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Functions.Worker.Http;

namespace ExportFuncAppUnitTestsXunit
{
    public class ExportFuncUnitTests2
    {
        [Fact]
        public async Task Http_trigger_should_return_known_string()
        {
            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 ExportFunc.Run(request.Object, context.Object);
            result.HttpResponse.Body.Seek(0, SeekOrigin.Begin);

            // Assert
            var reader = new StreamReader(result.HttpResponse.Body);
            var responseBody = await reader.ReadToEndAsync();
            Assert.NotNull(result);
            Assert.Equal(HttpStatusCode.OK, result.HttpResponse.StatusCode);
            Assert.Equal("Hello test", responseBody);
        }
    }
}

This results in a compilation error in ExportFuncUnitTests2.cs:

CS1061  'HttpResponseData' does not contain a definition for 'GetAwaiter' and no accessible extension method 'GetAwaiter' accepting a first argument of type 'HttpResponseData' could be found (are you missing a using directive or an assembly reference?)
for:
var result = await ExportFunc.Run(request.Object, context.Object);

The second solution given in the article involves FakeHttpRequestData.cs:

using System;
using System.Collections.Generic;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker;
using System.IO;
using System.Security.Claims;

namespace ExportFuncAppUnitTestsXunit
{
    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);
        }
    }
}

and, FakeHttpResponseData.cs:

using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker;
using System.Net;
using System.IO;

namespace ExportFuncAppUnitTestsXunit
{
    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; }
    }
}

And the test is (ExportFuncUnitTests2.cs):

using Xunit;
using ExportFuncApp;
using System.Threading.Tasks;
using System.IO;
using System.Text;
using Moq;
using Microsoft.Azure.Functions.Worker;
using System;
using System.Net;
using Microsoft.Extensions.Logging.Abstractions;

namespace ExportFuncAppUnitTestsXunit
{
    public class ExportFuncUnitTests2
    {
        [Fact]
        public async Task Http_trigger_should_return_known_string()
        {
            // 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 ExportFunc(new NullLogger<ExportFunc>());
            var result = await function.Run(request, context);
            result.HttpResponse.Body.Position = 0;

            // Assert
            var reader = new StreamReader(result.HttpResponse.Body);
            var responseBody = await reader.ReadToEndAsync();
            Assert.NotNull(result);
            Assert.Equal(HttpStatusCode.OK, result.HttpResponse.StatusCode);
            Assert.Equal("Hello test", responseBody);
        }
    }
}

The ExportFuncUnitTests2.cs has the following compilation errors:

CS1729  'ExportFunc' does not contain a constructor that takes 1 arguments
for:
var function = new ExportFunc(new NullLogger<ExportFunc>());

and

CS1503  Argument 2: cannot convert from 'Moq.Mock<Microsoft.Azure.Functions.Worker.FunctionContext>' to 'Microsoft.Azure.Functions.Worker.FunctionContext'
for:
var result = await function.Run(request, context);

I the article Guide for running C# Azure Functions in an isolated process was somewhat useful but no help in terms of unit testing. Maybe I am missing the point. Since there is no examples/documentation on how to do unit testing for AF 3.0 with .NET 5.0 from Microsoft then I should not be trying to do that?

Any pointers will be greatly appreciated. Thanks!

  • For second solution, initialize the class object as `var function = new ExportFunc();` and func as `var result = await function.Run(request, context.Object);` – user1672994 Nov 26 '21 at 10:11
  • I get the following compilation error: CS0176 Member 'ExportFunc.Run(HttpRequestData, FunctionContext)' cannot be accessed with an instance reference; qualify it with a type name instead –  Nov 26 '21 at 13:17
  • If I change the second line to: var result = await ExportFunc.Run(request, context.Object); then I get: CS1061 'HttpResponseData' does not contain a definition for 'GetAwaiter' and no accessible extension method 'GetAwaiter' accepting a first argument of type 'HttpResponseData' could be found (are you missing a using directive or an assembly reference? –  Nov 26 '21 at 13:21

1 Answers1

0

Your Run() function in ExportFunc is not awaitable. Therefore, you should replace the line:

var result = await function.Run(request, context)

with:

var result = function.Run(request, context).

If you want to test an awaitable function (i.e., async Run()), then you'd call it from your Unit Test with await.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77