5 July 2017

Writing unit tests for Azure Functions using C#

The most recent toolset for Azure Functions allows you to write compiled functions as .Net libraries using C#. This provides better start up performance, but also opens the entire ecosystem of Visual Studio tools for code analysis, third party extensions and unit testing.

The lack of support for common code production tooling has been a real barrier to entry for teams considering “serverless” architecture. Earlier versions of the Azure Functions tooling largely involved cutting and pasting code into a text box. Many developers are rightly suspicious of anything that they cannot unit test or incorporate into an automated code production pipeline.

Now that Azure Functions provides the tooling to create compiled C# functions and even debug them locally, it is possible to provide unit test coverage across an entire application. There are a few obstacles along the way though.

Static methods and dependencies

The main obstacle to unit testing Azure Functions is that they are static methods. This emphasises the functional nature of Azure Functions, i.e. instances should not share state, but it makes it hard to test code in isolation.

The example below shows a function that uploads the submitted data to Azure table storage.

[FunctionName("ExampleFunction")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "PUT")]HttpRequestMessage req, TraceWriter log)
{
    // Get request body
    var data = await req.Content.ReadAsAsync<UploadData>();
 
    // Create the Azure storage client - this could be mocked if we had an injection point
    var account = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["TableStorage"]);
    var client = account.CreateCloudTableClient();
 
    // Upload the data to Azure storage 
    CloudTable uploadTable = client.GetTableReference("Uploads");
    TableOperation insertOperation = TableOperation.Insert(data);
    await uploadTable.ExecuteAsync(insertOperation);
 
    // Return the response
    return req.CreateResponse(HttpStatusCode.OK);
}

This function has a hard dependency on the CloudTableClient object. The Azure SDK client objects have been marked as virtual for the most part so you can create a mock of this class using a framework like Moq. The problem is that the function’s static method signature does not provide you with any way of injecting different implementations on a per call basis.

One strategy for dealing with this involves setting up a shim that intercepts the call to the dependency so you can insert your own test code. Microsoft Fakes is the best-known implementation for shims in .Net, but it is only available to Visual Studio Enterprise users. Other frameworks such as JustMock and TypeMock’s Isolator can also provide shims but they both incur a license fee.

Another approach is to isolate the dependency behind a singleton factory class. This factory will return the correct dependency by default, though a unit test will be able to inject a mocked instance through the factory.

Passing stubbed trigger arguments

You can wire up Azure functions to a range of different triggers, and each of these give rise to different Run() method signatures. The majority of these are easy to incorporate into unit tests:

  • Azure storage queue and event hubs just require message content
  • Service bus functions can accept the message content or even a BrokeredMessage object from the service bus SDK
  • Blob triggers accept a filename and a Stream object containing the content

Other trigger types require a little more work to prepare valid input arguments.

HTTP Triggers

HTTP-based triggers need you to mock out an HTTP request which is a little trickier. You will need to provide an HttpRequestMessage instance but also make sure that an HttpConfiguration instance is associated with it so you can create responses.

public static HttpRequestMessage CreateRequest(string json)
{
    var request = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri("https://localhost"),
        Content = new StringContent(json, Encoding.UTF8, "application/json")
    };
 
    request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
 
    return request;
}

Note that to use HttpConfiguration your test project will need a reference to the Microsoft.AspNet.WebApi.Core NuGet package.

Timer Triggers

Functions that are triggered by a timer require a TimerInfo instance as an input argument. This is an abstract class so you will need to create your own stubbed version to pass it into a function during a unit test. This is a pretty simple implementation with a single method, i.e.

public class ScheduleStub : TimerSchedule
{
    public override DateTime GetNextOccurrence(DateTime now)
    {
        throw new NotImplementedException();
    }
}

Stubbing out the trace writer

The final input parameter in an Azure function is normally a TraceWriter argument which allows you to interact with the built-in function logging. This is an abstract class with no public constructor so it cannot be mocked. The easiest way to pass this is by creating a stubbed implementation – the example below shows a stubbed TraceWriter that records all the events pushed to it so they can be made available to a unit test.

public class TraceWriterStub : TraceWriter
{
    protected TraceLevel _level;
    protected List<TraceEvent> _traces;
 
    public TraceWriterStub(TraceLevel level) : base(level)
    {
        _level = level;
        _traces = new List<TraceEvent>();
    }
 
    public override void Trace(TraceEvent traceEvent)
    {
        _traces.Add(traceEvent);
    }
 
    public List<TraceEvent> Traces => _traces;
}

Note that this code will require your unit test project to have a reference to the Microsoft.Azure.WebJobs NuGet package to access the base TraceWriter class.

Filed under Architecture, C#, Microservices, Net Framework.