Integration Testing in Azure Functions with Dependency Injection
I’m a big proponent of integration and functional tests (or “subcutaneous tests” if there’s a UI). Done efficiently and sparingly, they give you the biggest bang for your buck when it comes to confidence in the overall well-being of the majority of your system.
Dependency injection is ubiquitous these days, and ASP.NET Core MVC’s seamless support for integration testing via the
Microsoft.AspNetCore.Mvc.Testing NuGet package has made this kind of testing simple when using dependency injection. However, I found that when dealing with Azure Function Apps, a similar setup is not as effortless as installing a package and using a
WebApplicationFactory, especially since Azure Function projects aren’t set up for dependency injection out of the box. Fortunately, after a bit of digging under the hood of
Microsoft.AspNetCore.TestHost, I’ve been able to create a similar testing experience which I’ll go through below.
The setup isn’t identical.
Microsoft.AspNetCore.Mvc.Testing bootstraps an in-memory test server that you can communicate with using an HTTP client, however, that’s part of ASP.NET Core while Azure Functions are built on top of the WebJobs SDK. While we can’t have that identical setup with functions, we can still meet our goal of setting up high-level integration tests while leveraging existing dependency injection setup and settings from our “real” functions without any duplication. We do this by operating at the level after HTTP by just bootstrapping a test
Sample Function App With Dependency Injection
Let’s assume the below Azure Function app, with one HTTP endpoint that responds with the answer to life, the universe and everything:
But we all know that
new is glue for dependencies, so let’s inject them. Support for dependency injection in Azure Functions has been added since Azure Functions 2.x. To register
ISuperComputer as services and inject them, we’ll create a
Startup class, similar to what we have with ASP.NET Core applications:
It has to inherit from
FunctionsStartup and you also need to add a
FunctionsStartupAttribute assembly attribute that points to the Startup class. Both of these types exist in the
Microsoft.Azure.Functions.Extensions NuGet package which you need to install.
Now we can have some dependency injection goodness with the Function Host automatically injecting the dependencies for us:
But how do we now properly test that our function correctly returns what we know is the obvious answer to life, the universe and everything? testing the various bits separately would cross into the realm of unit-testing, while duplicating our dependency injection setup is far from ideal; we want to keep and use all our existing DI configuration from our
Startup in our tests.
Setting Up An Azure Function Test Host
The secret to using our existing
Startup configuration and DI for integration testing lies in bootstrapping a .NET Core Generic Host via the
ConfigureWebJobs extension method:
The use of
ConfigureWebJobs might seem a bit strange, but since Azure Functions are built on top of the Web Jobs SDK, there is significant shared and compatible API surface. Here we are using the overload that takes an
Action<IWebJobsBuilder> configure argument. Our
Startup class inherits from
FunctionsStartup which inherits from
IWebJobsStartup, providing a compatible Configure method that calls our
Configure(IFunctionsHostBuilder builder) method!
Putting the above together, we can write an integration test, that initializes our HTTP Function reusing all the dependency injection and settings from our “real” code inside a test Host, and tests whether it correctly returns the true answer to life, the universe and everything:
If you’d like to quickly try it for yourself, I have shared my preferred way to set up Azure Function projects as a .NET Core Template on GitHub. The template includes a test set up with the above approach, plus using the familiar
appsettings.json file for settings instead of
local.settings.json and Environment Variables, to help with ease and simplicity of deployment, as well as a build defined via Azure YAML Pipelines.
I'm a software consultant, tech lead, servant team lead, C♯ and .NET aficionado, occasional public speaker, gardener, camping enthusiast and certified PSM I based in Brisbane, AU. I'm passionate about making positive impacts through technology, good software craftsmanship practices and effective project management.
Being a consultant exposes me to a wide variety of challenges, both technical and non-technical, and results in diverse and invaluable learning. This blog's main purpose is to serve as a self-reminder of some of those learnings that I've found the time to write down, but if it happens to help you too, that's even better!