Test Driven Development of Azure Functions with C# Part 1: Introduction to the application
Tags: csharp, dotnet, azure, serverless
Published: 2021-03-08 07:04:14

Over the last few years, Serverless computing has become one of the most appealing choice for developing backend application.  
With PaaS offerings like Azure App Service, we already had the benefit of not having to manage and maintain our Server infrastructure.  
The cloud service provider will do all the heavy lifting required, to manage the infrastructure, perform auto scaling and auto provisioning of the application.  

Now with FaaS offerings like Azure Functions, AWS Lambda, backend developers have been given the additional benefit of not having to maintain all the application server code.  
This is a very powerful abstraction, which makes perfect sense for event-driven applications. Once we write the piece of code that you want to execute for certain event(s), Azure Functions will do the hard work of  
- hosting this code (in some abstracted server infrastructure)
- provide the glue between the actual event and your application (ensure that your code actually gets triggered as and when the actual event gets triggered)
- auto-scale the application based on the volume of events received

For event driven applications that runs on the cloud, this is a very powerful model indeed.  

&nbsp;  
These days, we are seeing more and more organizations curating their backend systems running on the Cloud as a Microservice based architecture.  
In Azure for example, these microservices will leverage cloud services like Blob Storage, Message Queues, Service Bus, Event Hubs, Table Storage etc to communicate with each other.  

Azure Functions allows us to register our code to get notified for activities happening on these services, by registering to get notified for specific "events".  
For example, an Azure Function can get notified for a "Blob Event", that indicates that a blob was created at a certain path. Similarly, a message arriving on a Service Bus Topic can notify an Azure Function who has registered for it.  
The icing on the cake is that registration for a certain event is simply via adding event bindings of these services in the methos parameters - as simple as it gets!  

&nbsp;  
In this post, I would like to share a practical guide towards Test Driven Development (TDD) of Serverless Applications using Azure Functions.  
When I started developing serverless applications using Azure Functions, writing Unit Tests and Integration tests were two aspects in which I could not find good, detailed guides.  
Over the last few months, I do see ample resources guiding developers on how to Unit Test their Azure Functions app, but there is not much guidance on how to implement Integration Tests in these apps.  
In this series of posts, I would try to share my learnings, which hopefully would help future developers in their journey.  

This post will be the first of a three-part blog series, in which I shall introduce a sample Azure Functions application written in C#.  


&nbsp;  
___
**Other Posts in this series**

1. Part 1 - This Article
2. [Part 2 - Unit Tests](https://dev.to/sntnupl/test-driven-development-of-azure-functions-with-c-part-2-unit-tests-2ce8) 
3. [Part 3 - Integration Tests](https://dev.to/sntnupl/test-driven-development-of-azure-functions-with-c-part-3-integration-tests-5116)

Also, I have created this repository on Github, which contains all the associated source code used in this blog series:  

{% github sntnupl/azure-functions-sample-unittest-integrationtest no-readme %}

&nbsp;  
___
### The Sample Application

Our sample application is an Azure Function, which will get triggered by an event triggered by Azure Service Bus, when a message is sent to Service Bus Topic.
This message essentially contains location to a blob in  Azure Blob Storage. 
Once our app is able to read the text contents of this blob, it will try to parse its contents as (fictious) Invoice entries of Acme Corp. 
These parsed Acme Corp invoice entries will then be persisted in Azure Table Storage. 

If you have worked with Azure functions a question you might be wondering, is that instead of using Service Bus trigger, why don't we use the trigger generated by the Blob storage itself.
That can be a valid solution, in certain use cases. I chose to architect our application such that we read the location from a service bus message because of the following reasons:

1. I can send additional metadata about the upload blob in a cleaner fashion. For e.g., I can pass metadata like `transaction-id`, `user-id` in the Service Bus message

1. I can choose to be flexible on the actual location of the uploaded file (what if some client wants to use AWS S3?)
2. Another application who wants these uploaded invoices to be processed, can choose to delay the processing, or may even want to batch them up. Both of these can be easily achieved if we choose Service Bus Trigger. 

&nbsp;  
Project **InvoiceProcessor** hosts our Azure Function. inside `InvoiceWorkitemEventHandler.cs`.

```csharp
[FunctionName("InvoiceWorkitemEventHandler")]
public static async Task Run(
    [ServiceBusTrigger("workiteminvoice", "InvoiceProcessor", Connection = "ServiceBusReceiverConnection")] string msg,
    IBinder blobBinder,
    [Table("AcmeOrders", Connection = "StorageConnection")] IAsyncCollector<AcmeOrderEntry> tableOutput,
    ILogger logger)
{
...
}
```

The line `[ServiceBusTrigger("workiteminvoice", "InvoiceProcessor", Connection = "ServiceBusReceiverConnection")] string msg,` shows just how easy it is, to hook to event triggers generated from Azure Service Bus.  

&nbsp;  
All we need is to use the `ServiceBusTrigger` attribute to decorate the 1st method parameter.  
It takes 3 arguments itself: 
- *Topic Name*, 
- *Subscription Name*, and 
- the name of the Configuration property, that stores the ServiceBus *Connection* string.  

If you want more details on this attribute, I would suggest you to go through [this article](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-trigger?tabs=csharp) - which explains the Azure Service Trigger in more details.

&nbsp;  
The next method parameter to our Azure Function is an `IBinder`.  
We will be using this to perform input-binding (i.e., bind to perform read operations) to a particular blob in Azure Blob Storage.  

`ServiceBusTrigger` allowed us to imperatively perform input-binding to Service Bus messages. This works because we know that we will always be looking for messages in `workiteminvoice` topic, using `InvoiceProcessor` subscription.  
We can't use the corresponding `Blob` Trigger - because we will know about the blob location at runtime, ONLY after we parse the message received from Service Bus.

For scenarios like these, when we need run time binding we can use the `IBinder` input parameter.  
We will soon see, that once we know the blob location, how we can use `BindAsync` method on the `IBinder` instance to perform input binding to the blob.

&nbsp;  
The last parameter, allows us to perform output binding to Azure Table Storage: `[Table("AcmeOrders", Connection = "StorageConnection")] IAsyncCollector<AcmeOrderEntry> tableOutput`.  
Imperative binding works here, because we know we will always be writing to the table named `AcmeOrders`, withing the Azure Storage account whose connection string is specified in the configuration property `StorageConnection`.  

Also, since we might be writing (output-ing) multiple entries to this table, we need to use `IAsyncCollector<T>` as the concrete type. Think of these as `out` parameters to an async Azure Function.   
We will be invoking `AddAsync()` method on this to write multiple entries to the Azure Table Storage.  


&nbsp;  
___
### The Code  

Let us now take a look into the first part of this function:

```csharp
InvoiceWorkitemMessage invoiceWorkItem;
logger.LogInformation($"C# ServiceBus topic trigger function processed message: {msg}");

if (string.IsNullOrEmpty(msg)) {
    logger.LogError("Empty Invoice Workitem.");
    return;
}

try {
    invoiceWorkItem = JsonSerializer.Deserialize<InvoiceWorkitemMessage>(msg, jsonSerializationOptions);
}
catch (JsonException ex) {
    logger.LogError(ex, "Invalid Invoice Workitem.");
    return;
}
catch (Exception ex) {
    logger.LogError(ex, "Unexpected Error.");
    return;
}

if (invoiceWorkItem.DataLocationType != DataLocationType.AzureBlob) {
    logger.LogError($"Unsupported data location type {invoiceWorkItem.DataLocationType}.");
    return;
}

if (string.IsNullOrEmpty(invoiceWorkItem.DataLocation)) {
    logger.LogError("Empty data location.");
    return;
}
```  
Nothing too fancy here. We are essentially trying to parse the message received from Service Bus as an `InvoiceWorkitemMessage`.  
If the message is valid, and we are able to parse it, we check that the blob location is of type Azure Blob Storage (currently we support ONLY Azure Blob Storage).  

&nbsp;  
Once we know the blob path, we can use the `IBinder` instance to bind to the specific blob path. As discussed earlier, we will be using the `BindAsync` method to get a `Stream` of the blob contents.  

```csharp
var blobAttr = new BlobAttribute(invoiceWorkItem.DataLocation, FileAccess.Read) {
    Connection = "StorageConnection"
};

using (var blobStream = await blobBinder.BindAsync<Stream>(blobAttr)) {
   //...
}
```

&nbsp;  
We will then parse the contents of this blob, using `InvoiceParser` class instance.  

```csharp
if (!InvoiceParser.TryParse(blobStream, logger, out List<AcmeOrder> orders)) {
    logger.LogError("Failed to parse invoice.");
    return;
}
```

I will skip the inner behavior of the `TryParse` method, as it doesn't really add to our scope of discussion here. 
You can look into the `InvoiceParser.cs` file for more details.

&nbsp;  
If everything goes fine, and we are able to parse the contents of the blob as one or more `AcmeOrder`s, we can persist them in Azure Table Storage.
As discussed earlier, we use `AddAsync()` method on the `IAsyncCollector<AcmeOrderEntry>` to achieve this.

```csharp
foreach (var order in orders) {
    await tableOutput.AddAsync(new AcmeOrderEntry(invoiceWorkItem.UserEmail, order));
    logger.LogInformation($"Added table record for BigBasket Order number: {order.OrderNumber}");
}
```

&nbsp;  
To perform TDD with this sample application, we need to write Unit Tests and Integration Tests against this code. 

&nbsp;  
___
In the [next post](https://dev.to/sntnupl/test-driven-development-of-azure-functions-with-c-part-2-unit-tests-2ce8), we will discuss how we can implement Unit Tests for this Azure Function. See you there!