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.

My preferred approach for dealing with this is to move any meaningful functionality out of the actual function definition and run it in a separate library. This means that the function definition is just a bootstrapper that reads configuration, wires up dependencies and handles exceptions. Any meaningful business logic is shunted into a separate library where it can be properly unit tested in isolation.

An advantage of this approach is that it helps to guard against the implementation lock-in that is inherent with this kind of proprietary run-time environment. You can move the execution context to a service or API if you decide that Azure functions are not for you. The downside is that this increases complexity as you’ll have to maintain a layer of “dumb” bootstrap functions on top of the libraries that do all the meaningful work in your system.

If you prefer to keep your functions isolated then you could consider setting up a shim that intercepts calls to any dependency, allowing you to 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.

A third approach is to isolate dependencies behind singleton factory classes. These factories can return the correct dependency by default, though a unit test will be able to inject a mocked version into the function via 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, Azure, C#, Microservices, Net Framework.