- Is Your Serverless Application Testable? – Azure Functions
- Is Your Serverless Application Testable? – Azure Logic Apps
In this post, I’m going to introduce several design patterns when we write Azure Functions codes in C#, by considering full testabilities.
Sample codes used in this post can be found here.
User Story
- As a DevOps engineer,
- I want to search ARM templates using Azure Functions
- So that I can browse the search result.
Basic Function Code
We assume that we have a basic structure that Azure Functions can use. Here’s a basic function code might look like:
As you can see, all dependencies are instantiated and used within the function method. This works perfectly fine so there’s no problem at all. However, from a test point of view, this smells A LOT. How can we refactor this code for testing?
I’m going to introduce several design patterns. They can’t be overlooked, if you’ve been working with OO programming. I’m not going to dive into them too much in this post, by the way.
Service Locator Pattern
As you can see the code above, each and every Azure Functions method has the static
modifier. Because of this restriction, we can’t use a normal way of dependency injections but the Service Locator Pattern. With this, the code above can be refactored like this:
First of all, the function code declares the IServiceLocator
property with the static
modifier. Then, the Run()
method calls the service locator to resolve an instance (or dependency). Now, the method is fully testable.
Here’s a simple unit test code for it:
The Mock
instance is created and injected to the static property. This is the basic way of dependency injection for Azure Functions. However, there’s still an issue on the service locator pattern.
Mark Seemann argues on his blog post that the service locator pattern is an anti pattern because we can’t guarantee whether dependencies that we want to use have already been registered or not. Let’s have a look at the refactored code again:
Within the Run()
method, the IGitHubService
instance is resolved by this line:
var service = ServiceLocator.GetInstance();
When the amount of codes is relatively small, that wouldn’t be an issue. However, if the application grows up quickly and has become a fairly large one, can we ensure if the service
variable is always NOT null
? I don’t think so. We have to use the service locator pattern, but at the same time, try not to use it. That doesn’t sound quite right, though. How can we achieve this goal?
Strategy Pattern
Strategy Pattern is basically one of the most popular design patterns and can be applied to all Functions codes. Let’s start from the IFunction
interface.
Every function now implements the IFunction
interface and all logics written in a trigger function MUST move into the InvokeAsync()
method. Now, the FunctionBase
abstract class implements the interface.
While implementing the IFunction
interface, the FunctionBase
class add the virtual
modifier to the InvokeAsync()
method so that other functions class inheriting this base class will override the method. Let’s put the real logic.
The GetArmTemplateDirectoriesFunction
class overrides the InvokeAsync()
method and processes everything within the method. Dead simple. By the way, what is the generic type of TOptions
? This is the Options Pattern that we deal with right after this section.
Options Pattern
Many API endpoints have route variables. For example, if an HTTP trigger’s endpoint URL is /api/products/{productId}
, the productId
value keeps changing and the value can be handled by the function method like Run(HttpRequestMessage req, int productId, ILogger log)
. In other words, route variables are passed through parameters of the Run()
method. Of course each function has different number of route variables. If this is the case, we can pass those route variables, productId
for example, through an options
instance. Code will be much simpler.
There is an abstract class of FunctionParameterOptions
. The GetArmTemplateDirectoriesFunctionParameterOptions
class inherits it, and contains a property of Query
. This property can be used to store a querystring value for now. The InvokeAsync()
method now only cares for the options instance, instead of individual route variables.
Builder Pattern
Let’s have a look at the function code again. The IGitHubService
instance is injected through a constructor.
Those dependencies should be managed somewhere else, like an IoC container. Autofac and/or other IoC container libraries are good examples and they need to be combined with a service locator. If we introduce a Builder Pattern, it would be easily accomplished. Here’s a sample code:
With Autofac
, all dependencies are registered within the RegisterModule()
method and the container is wrapped with AutofacServiceLocator
and exported to IServiceLocator
within the Build()
method.
Factory Method Pattern
All good now. Let’s combine altogether, using the Factory Method Pattern.
Within the constructor, all dependencies are registered by the ServiceLocatorBuilder
instance. This includes all function classes that implement the IFuction
interface. FunctionFactory
has the Create()
method and it encapsulates the service locator.
This function now has been refactored again. The IServiceLocator
property is replaced with the FunctionFactory
property. Then, the Run
method calls the Create()
method and InvokeAsync()
method consecutively by fluent design approach. At the same time, GetArmTemplateDirectoriesFunctionParameterOptions
is instantiated and passed the querystring value, which is injected to the InvokeAsync()
method.
Now, we’ve got all function triggers and its dependencies are fully decoupled and unit-testable. It has also achieved to encapsulates the service locator so that we don’t even care about that any longer. From now on, code coverages have gone up to 100%, if TDD is properly applied.
So far, I’ve briefly introduced several useful design patterns to test Azure Functions codes. Those patterns are used for everyday development. This might not be the perfect solution, but I’m sure this could be one of the best practices I’ve found so far. If you have a better approach, please send a PR to the GitHub repository.
Azure Logic Apps will be touched in the next post.
For the last X number of years, we’ve been told that Service Locator is a horrible anti-pattern. Now it’s the answer to working with Azure Functions. It’s not credible.
@ericbl Thanks for your comment!
Well, I agree that service locator is still an anti-pattern because of the reason stated in this post. If there is a better way to manage dependencies without relying on service locator, I would take that approach.
I would have a look at Boris Wilhelm’s implementation that leverages the custom binding functionality in v2.0. It allows you to specify dependencies as function arguments. It uses the built-in DI stuff in .Net Core rather than service location.
Linky: https://github.com/BorisWilhelms/azure-function-dependency-injection