In the previous post of this series, we saw how one can implement Unit Tests for an Azure Function.
While Unit Tests forms the base of our Testing Pyramid, they are not enough from TDD completion point of view.
The next phase would be to implement Integration Tests for our Azure Function.
Other Posts in this series
- Part 1 - Introduction to our sample Azure Functions application
- Part 2 - Unit Tests
- Part 3 - This Article
I have created this repository on Github, which contains all the associated source code used in this blog series:
sntnupl / azure-functions-sample-unittest-integrationtest
Sample application to showcase how one can implement Unit and Integration testing for an Azure Functions application
The relevant Code for this post is under InvoiceProcessor.Tests.Integration project in this repository.
Few of the characteristics that would separate these Integration tests from Unit Tests:
- We would not mock the Event Triggers from Service Bus. We will make our Azure Function to get triggered by real messages coming from Service Bus Topic.
- We would not be mocking the
IBinder
that our Azure function uses to read from Blob storage - we will use actual blob storage - We would not be mocking the
IAsyncCollector
to write to Azure Table Storage. We will be using real Table Storage that our Azure Function will write to, and we will be verifying those writes by reading from those real Table Storage.
As you can imagine, to achieve these feats, we would need to do some background work first.
Before going into the details of those "background work", let us understand some concepts first.
Azure WebJobs SDK and how it is related to Azure Functions
When we execute an Azure Functions app, it will start a console app.
However, the actual "function" can't operate on its own and magically listen for event triggers - there is no magic. It needs a runtime container, a Host - which has the capability of listening for event triggers and executing this Azure Function.
This host will be responsible for starting the Azure Functions app, and managing its lifetime. It will also set up Dependency Injection, Logging and Configuration for the Azure Function app.
This host is implemented via the JobHost
class in Azure WebJobs SDK (source code here). Azure Functions is built on top of the Azure WebJobs SDK package, which allows it to use this JobHost
class.
Of course, when we implement an Azure Function app, or even execute it, all of the details related to the JobHost
will be abstracted away from us.
Inside an Azure Functions project, the only knobs you get to the Host is via the host.json
file. A complete reference to all the possible host settings that can be tweaked via host.json is available at this link.
So once we understand this relationship, we can see that in order to setup our Integration Testing environment, we would need to
- bring up this host
- Tell it where to look for the Azure Functions to load
With this knowledge under our belt, lets go through our Integration Test project.
Note: All code discussed in the following sections is in the InvoiceProcessor.Tests.Integration Project.
Designing the Integration Tests
The SUT for our Integration Test is the Azure Function inside InvoiceWorkitemEventHandler
class. This function essentially does 3 things:
- Gets triggered as and when a new message arrives in a specific Azure Service Bus Topic, and parses this message to extract the location of an uploaded invoice
- Reads the blob from the above extracted location, and parses the content of the uploaded invoice file
- Finally it persists the content in a specific format inside Azure Table Storage
As such, our Integration Tests will need to
- Upload a dummy invoice file into an Azure Blob Storage Container
- Create message with the location of the above created blob and send it to the Service Bus Topic our Azure Function is listening to
- Finally, wait for the corresponding entries to be created in the Azure Table Storage
Before doing these tasks, however, our Tests must ensure that any artifacts from previously run tests are cleaned off. In other words, it needs to:
- Delete any Azure Blob Storage containers that previous tests would have created
- Create a new Azure Blob Storage container that will be used by the tests in the current run
- Delete any entries inside Azure Table Storage that was created by previously running tests.
The entity that takes care of these tasks is the EndToEndTestFixture
class.
We leverage the Class Fixture pattern with this class, which allows us to create test context that will get shared across all tests in the ValidInvoiceTests
test suite.
This class inherits from the IAsyncLifetime
interface of xUnit as well, which allows us to perform asynchronous setup tasks during its initialization.
Writing our Integration Tests
Arrange Part
[Fact]
public async Task ValidInvoiceUploaded_GetsParsedAndSavedToTableStorage()
{
string blobPath = await _testFixture.UploadSampleValidInvoice();
// ...
}
Before doing anything else, we are uploading our sample invoice file to Azure Blob Storage.
[Fact]
public async Task ValidInvoiceUploaded_GetsParsedAndSavedToTableStorage()
{
// ...
IHost host = new HostBuilder()
.ConfigureWebJobs()
.ConfigureDefaultTestHost<InvoiceWorkitemEventHandler>(webjobsBuilder => {
webjobsBuilder.AddAzureStorage();
webjobsBuilder.AddServiceBus();
})
.ConfigureServices(services => {
services.AddSingleton<INameResolver>(resolver);
})
.Build();
using (host) {
await host.StartAsync();
// ..
}
// ...
}
This is the part where we set up a Host that will contain our Azure Functions.
Specifically we have used the ConfigureDefaultTestHost<InvoiceWorkitemEventHandler>()
helper method to tell the host to look for Azure Functions inside InvoiceWorkitemEventHandler
class.
We have also initialized the Azure Storage (AddAzureStorage()
) as well as the ServiceBus (AddServiceBus()
) extensions, as both of these are used by our SUT.
once the host is created, we invoke StartAsync()
to start it.
Act Part
public async Task ValidInvoiceUploaded_GetsParsedAndSavedToTableStorage()
{
// ...
using (host) {
// ...
await _testFixture.SendInvoiceWorkitemEvent(blobPath);
// ...
}
}
Here we create a message with the location of the uploaded invoice file, and send it to the Azure Service Bus Topic.
As soon as we push this message, our SUT should get triggered, and if everything goes well, Azure Table Storage would get updated with corresponding entries.
Assert Part
public async Task ValidInvoiceUploaded_GetsParsedAndSavedToTableStorage()
{
// ...
using (host) {
// ...
List<AcmeOrderEntry> parsedOrders = await _testFixture.WaitForTableUpdateAndGetParsedOrders();
_output.WriteLine($"Found {parsedOrders.Count} invoices.");
Assert.True(parsedOrders.Count == 2);
}
}
Here we are waiting for the entries to be uploaded in the Table Storage, and asserting that 2 entries were created (our sample invoice contained two entries).
Similar to running Unit Tests, we can use the Test Explorer in Visual Studio to execute this Integration Test as well.
Summary
In this series of post, we saw how we can effectively perform Test Driven Development of Azure Functions.
I have often seen developers worry about this aspect, when it comes to Azure Functions.
The fact that these functions involves triggeres external entities like Azure Blob Storage, Azure Service Bus, etc, makes developers believe that unit/integration testing the Azure Functions will be a complex endeavour.
As we have seen in this series of posts, that notion is not necessarily true.
I hope that this series helped you to embrace TDD in your Azure functions app, and hopefully helped you become a better developer by shipping higher quality code.
Thanks for reading! 🙂
Top comments (1)
Thanks a lot!
I have a question: Why the following in the HostBuilder, is this needed or just as an example?