In Part 1 of this series we started to explore how to implement Test Driven Development of Serverless Azure Functions application.
We had an overview of some of the Event Bindings that allow Azure Functions to integrate with Azure Services like Azure Blob Storage, Azure Service Bus, etc.
In this post, we will see how we can add Unit Tests to our Azure Functions application.
Other Posts in this series
- Part 1 - Introduction to our sample Azure Functions application
- Part 2 - This Article
- Part 3 - Integration Tests
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.Unit project in this repository.
Adding Unit Test Project
We will be using xUnit Test Framework to implement Unit Tests for our Azure Function.
You can visit this official document, on how to get started with creating a new xUnit Test Project via Visual Studio.
Refer to InvoiceProcessor.Tests.Unit
project within our Github repository, for all the relevant code discussed in this post.
File InvoiceWorkitemEventHandlerShould.cs
hosts all the tests that we will use to Unit Test our Azure Function.
Ground work
Before writing the test cases, we would need to do create some resources that we are going to need for our testing. Let's take a look at them.
Mocked Logger
We need to pass an instance of ILogger
to our Azure Function.
Going forward, let's address this as SUT.
To aid in our testing, we have created our own implementation of ILogger
.
This implementation is available in ListLogger.cs
class within our project.
Some implementations of Unit Testing uses NullLoggerFactory
to create an instance of ILogger
and pass the same in the Azure Function.
That works for sure, but in our testing strategy, we intend to capture the logs generated by our SUT to validate sucess/failure of our test cases.
As such we have created ListLogger
, which essentially stores all the published logs from SUT into a List<string>
. Within our test cases we will be checking the contents of this list to Assert certain behaviors.
Mocked AsyncCollector
Our SUT takes IAsyncCollector<AcmeOrderEntry>
as its paramter. So we need to create a mock for this as well.
To aid in our testing, we have created our own implementation of IAsyncCollector
, called AsyncCollector
- you can refer to AsyncCollector.cs
for the code.
This class essentially wraps over a List<T>
as well. Any invocation to AddAsync()
will be adding items to this internal List.
This again, allows us to verify that SUT invoked AddAsync()
properly on the IAsyncCollector<AcmeOrderEntry>
that got passed to it.
Other Mocks
Apart from the above, we have mocked the following entities in our Test Project
-
IBinder
: this is passed as the method parameter to SUT -
IAcmeInvoiceParser
- This is used within the SUT code, to parse the blob text.
- To keep things simple, we have kept this as a publicly accessible static property within
InvoiceWorkitemEventHandler
class, instead of using Dependency Injection.
All these mocks is created within the constructor of our Test Suite, the InvoiceWorkitemEventHandlerShould
class.
XUnit has few powerful strategies to allow developers share these resources as a test context. I would suggest going through the official xUnit documentation on Shared Context between Tests, for more details.
To put things briefly, we wanted to recreate these mocks afresh for every test, hence we created them within the constructor of our Test Suite.
Writing our first Unit Test
While writing these test cases, we must not loose sight of the fact that at the end of the day, an Azure Function is just a simple method which can be invoked as any normal function.
public static async Task Run(
[ServiceBusTrigger("workiteminvoice", "InvoiceProcessor", Connection = "ServiceBusReceiverConnection")] string msg,
IBinder blobBinder,
[Table("AcmeOrders", Connection = "StorageConnection")] IAsyncCollector<AcmeOrderEntry> tableOutput,
ILogger logger)
So although we see the 1st parameter string msg
decorated with the ServiceBusTrigger
attribute, we dont need to do any extra work with respect to this attribute.
To simulate a message coming this Azure Function via the Service Bus, we just pass a string in the 1st parameter, thats it!
With that being said, lets go through one of the test cases which validates that an invalid message arriving via Service Bus would get rejected by our SUT.
This test is housed with RejectInvalidMessagesFromServiceBus()
method of InvoiceWorkitemEventHandlerShould
class.
Arrange Part
public async Task RejectInvalidMessagesFromServiceBus()
{
_mockParser
.Setup(x => x.TryParse(
It.IsAny<Stream>(),
_testLogger,
out _mockParsedOrders))
.Callback(new TryParseCallback((Stream s, ILogger l, out List<AcmeOrder> o) => {
o = new List<AcmeOrder>();
}))
.Returns(true);
//...
}
Here we are setting up our mocked IAcmeInvoiceParser
. We have used the wonderful moq package to create these mocks.
This code essentially states that whenever SUT invokes TryParse()
method on our mock, return a value of true
.
- We dont really care about the
Stream
parameter that the SUT passes toTryParse()
-It.IsAny<Stream>()
- But we do expect the SUT to pass our very own
ILogger
implementation (ListLogger
) to be passed inTryParse()
-_testLogger
- The
Callback()
part is to essentially mock-populate theout
parameter that SUT will be passing toTryParse()
method.
public async Task RejectInvalidMessagesFromServiceBus()
{
// ...
_mockBlobBinder
.Setup(x => x.BindAsync<Stream>(
It.IsAny<BlobAttribute>(),
default))
.Returns(Task.FromResult<Stream>(null));
var sut = new InvoiceWorkitemEventHandler();
InvoiceWorkitemEventHandler.InvoiceParser = _mockParser.Object;
// ...
}
Here we are setting up our mocked IBinder
instance.
SUT can pass any BlobAttribute
, and default
value of CancellationToken
(which is null), and our mock will return a Task<stream>
with value null
.
We have also created an instance of InvoiceWorkitemEventHandler
class and injected our mocked IAcmeInvoiceParser
to its public static property.
Act Part
public async Task RejectInvalidMessagesFromServiceBus()
{
// ...
await InvoiceWorkitemEventHandler.Run(
"Invalid work item",
_mockBlobBinder.Object,
_mockCollector,
_testLogger);
// ...
}
We invoke the SUT, with an invalid message "Invalid work item"
.
Assert Part
public async Task RejectInvalidMessagesFromServiceBus()
{
//...
var logs = _testLogger.Logs;
logs.Should().NotBeNull();
logs.Should().NotBeEmpty();
logs.Should().NotContain(l => l.Contains("Empty Invoice Workitem."));
logs.Should().Contain(l => l.Contains("Invalid Invoice Workitem."));
_mockBlobBinder.Verify(b => b.BindAsync<Stream>(It.IsAny<BlobAttribute>(), default), Times.Never);
}
As we mentioned earlier, we will be leveraging our ListLogger
class to verify SUT behavior.
Inside the SUT test code, we first check if the message from Service Bus is empty. If it is, the SUT logs message Empty Invoice Workitem.
and exit.
Then it checks if the message from Service Bus is invalid. If it is it will log message Invalid Invoice Workitem.
and exit.
What we have asserted here is that the first message is not logged by SUT, as message was not empty: logs.Should().NotContain(l => l.Contains("Empty Invoice Workitem."));
However the second message should get logged, becuase the message was an invalid InvoiceWorkitemMessage
: logs.Should().Contain(l => l.Contains("Invalid Invoice Workitem."));
Lastly, we also assert that SUT must not invoke BindAsync
on the IBinder
passed to it, because the method should have exited.
Here, we have leveraged the FluentAssertions package to write the descriptive Assertions messages.
To execute test cases like these, use the Test Explorer in Visual Studio.
In the last post of this series, we will see how we can perform Integration Testing on our Azure Function.
Top comments (0)