2
votes

So far I've been able to setup unit testing for Azure Functions and it works great. However for my current project I need to use dynamic or imperative bindings. https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-csharp#imperative-bindings

This leads to issues for my unit test I cannot seem to solve.

My function looks like this:

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.ServiceBus.Messaging;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace My.Functions
{
    public static class MyFunc
    {
        [FunctionName("my-func")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req,
            Binder binder)
        {
            dynamic data = await req.Content.ReadAsAsync<object>();
            byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data));
            MemoryStream stream = new MemoryStream(bytes, writable: false);

            var sbMsg = new BrokeredMessage(stream) { ContentType = "application/json" };
            var attributes = new Attribute[]
            {
                new ServiceBusAccountAttribute("some-sb-account"),
                new ServiceBusAttribute("some-queue-or-topic", AccessRights.Send)
            };
            var outputSbMessage = await binder.BindAsync<IAsyncCollector<BrokeredMessage>>(attributes);
            await outputSbMessage.AddAsync(sbMsg);

            return req.CreateResponse(HttpStatusCode.OK, "OK");
        }
    }
}

Near the end of the code of the function, I configure this binder to hold a list of BrokeredMessages. This is done by calling the BindAsync on the binder.

The attributes are dynamically set and contain a servicebus connection and topic name. This all works great when deployed to Azure so functionality-wise everything is fine. So far so good.

However I'm stuggling with getting my test running. To be able to invoke the function, I need to provide parameters. The HttpTrigger this is pretty common, but for the Binder I don't know what to provide.

For testing I use this approach:

[TestMethod]
public void SendHttpReq()
{
    // Setup
    var httpRequest = GetHttpRequestFromTestFile("HttpRequest");
    var sbOutput = new CustomBinder();

    // Act
    var response = SendToServicebus.Run(httpRequest, sbOutput);

    // Assert
    Assert.AreEqual(sbOutput.Count(), 1);

    // Clean up
}

I use a CustomBinder inherited from Binder, because just having an instance of Binder failed in the function on the 'BindAsync' throwing 'Object reference not set to an instance of an object'. It seems the constructor of the binder is actually not meant to be called.

In the CustomBinder I override the BindAsync to return a generic list of BrokeredMessages.

public class CustomBinder : Binder
{
 public override async Task<TValue> BindAsync<TValue>(Attribute[] attributes, CancellationToken cancellationToken = new CancellationToken())
 {
  return (TValue)((object)(new List<BrokeredMessage>()));
  }
}

Not entirely surprising that also failed throwing:

InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List'1[Microsoft.ServiceBus.Messaging.BrokeredMessage]' to type 'Microsoft.Azure.WebJobs.IAsyncCollector`1[Microsoft.ServiceBus.Messaging.BrokeredMessage]'.

I cannot find an implementation of the IAsyncCollector, so maybe I need to approach this differently?

My actual goal is to be able to verify the list of brokered messages, as the function would output to Azure servicebus.

1
There is IBinder interface, but it's more limited than Binder AFAIK. Does it have the method that you need? Why not mock IAsyncCollector too?Mikhail Shilkov
I tried the IBinder, but that one doesn't contain a method to provide an array of attributes and that's what I need.Jean-Paul Smit
The goal I'm trying to achieve is to be able to read the list of brokered messages as it's returned from the Function. I'd like to validate whether the message contents are as expected after running the Function.Jean-Paul Smit
That's why you could mock IAsyncCollector. Verifying BrokeredMessage might be challenging though, not even sure if you can read message content out of it.Mikhail Shilkov
In a non-binder scenario you use ICollector<BrokeredMessage> in the signature and then it's easy to test this. For Binder it really is a challenge.Jean-Paul Smit

1 Answers

0
votes

As mentioned in the comments, I would agree that mocking it makes sense. You explicitely want to unit test your own code logic. With only your own business logic in mind, you may assume that the actual the actual remote operation binder.BindAsync(...) - of which you have no control over - works as expected.

Mocking it in a unit test should work with something like this:

using FluentAssertions;
using Microsoft.Azure.WebJobs;
using Microsoft.ServiceBus.Messaging;
using Xunit;

[Fact]
public async Task AzureBindAsyncShouldRetrunBrokeredMessage()
{
    // arrange           
    var attribute = new ServiceBusAccountAttribute("foo");
    var mockedResult = new BrokeredMessage()
    {
        Label = "whatever"
    };

    var mock = new Mock<IBinder>();
    mock.Setup(x => x.BindAsync<BrokeredMessage>(attribute, CancellationToken.None))
        .ReturnsAsync(mockedResult);

    // act
    var target = await mock.Object.BindAsync<BrokeredMessage>(attribute);

    // assert
    target.Should().NotBeNull();
    target.Label.Should().Be("whatever");
}

I understand that your concern may be a full integration test. You seem to want to test the entire chain. In that case, having a unit test might prove difficult because you depend on an external system. If that is the case you might want to create a seperate integration test on top of it, by setting up a seperate instance.

Considering your function is setup as a HttpTrigger, the following should work:

# using azure functions cli (2.x), browse to the output file
cd MyAzureFunction/bin/Debug/netstandard2.0

# run a new host/instance if your function
func host start 

Next, simply execute a http request to the hosted endpoint:

$ [POST] http://localhost:7071/api/HttpTriggerCSharp?name=my-func

In this case you have a clean and isolated integration setup.

Either way, I'd like to argue to either go the route of the unit test with mock OR setting up a seperate integration test setup for it.

Hope this helps...