Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 532
Vincent Bitter Avatar asked Jun 11 '21 13:06

Vincent Bitter


People also ask

Do Azure functions support .NET 5?

As of March 2021, Microsoft announced that Azure Functions are supported running on . NET 5.

How do I test Azure function app?

Navigate to https://functions.azure.com and click on “Login to your account". Navigate to https://functions.azure.com and provide Azure Functions details to create the Azure Functions. Navigate to https://portal.azure.com, click on "New" from Azure portal > Web + Mobile >click on see All or Search > Function App.

Can you unit test Azure functions?

When executing your Azure Functions, the functions runtime will run your function code with a concrete implementation of these interfaces. For unit testing, you can pass in a mocked version of these interfaces to test your business logic.

Can I test Azure function locally?

When you use Functions, using your favorite code editor and development tools to create and test functions on your local computer becomes easier. Your local functions can connect to live Azure services, and you can debug them on your local computer using the full Functions runtime.


1 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);
like image 130
Vincent Bitter Avatar answered Sep 22 '22 06:09

Vincent Bitter