DEV Community

loading...

Test Driven Development of Azure Functions with C# Part 2: Unit Tests

sntnupl profile image Santanu Paul Originally published at sntnupl.com Updated on ・5 min read

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

  1. Part 1 - Introduction to our sample Azure Functions application
  2. Part 2 - This Article
  3. Part 3 - Integration Tests

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

GitHub logo 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)
Enter fullscreen mode Exit fullscreen mode

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);

    //...
}
Enter fullscreen mode Exit fullscreen mode

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 to TryParse() - It.IsAny<Stream>()
  • But we do expect the SUT to pass our very own ILogger implementation (ListLogger) to be passed in TryParse() - _testLogger
  • The Callback() part is to essentially mock-populate the out parameter that SUT will be passing to TryParse() 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;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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);

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Visual Studio Test Explorer

In the last post of this series, we will see how we can perform Integration Testing on our Azure Function.

Discussion (0)

pic
Editor guide