Unit testing BlobServiceClient with Azure blobs and NSubstitute

I have blogged a whole lot about unit testing lately. Today's post is no exception. I recently migrated some of the last code we had running on the Microsoft.Azure.Storage.Blob package to Azure.Storage.Blobs. During that task, I had to rewrite some unit tests using NSubstitute for mocking communication with Azure Blob Storage. In this post, I'll show how to do just that.

Unit testing BlobServiceClient with Azure blobs and NSubstitute

Unit testing code that uses the BlobServiceClient class from the Azure.Storage.Blobs package is a bit harder than it could have been. All of the methods that you'd want to mock are declared as virtual which makes it possible to easily replace the implementation using a mocking framework like NSubstitute.

For the rest of this post, I'll write a unit test of a piece of code doing cleanup in blob containers. Consider a system that creates a blob container per day to store log data or similar. This will create containers like these:

  • 2024-04-16
  • 2024-04-15
  • 2024-04-14
  • etc.

The cleanup code can be implemented in a new class named BlobCleanup:

public class BlobCleanup(BlobServiceClient blobClient)
{
    private readonly BlobServiceClient blobClient = blobClient;
    
    public void Cleanup()
    {
        var containerToSave = DateTime.Today.ToString("yyyy-MM-dd");
        var blobContainers = blobClient
            .GetBlobContainers()
            .Select(container => container.Name)
            .Where(name => name != containerToSave);
        foreach (var name in blobContainers)
        {
            // Remove container
            blobClient.DeleteBlobContainer(name);
        }
    }
}

The code fetches all containers with a name not matching today and deletes them. This is just a simple example for the sake of showing how to write the unit test.

Let us create the first lines of the unit test setting up the blob client. I'll use NUnit and NSubstitute for the test, but feel free to pick your framework of choice since none of the code will use special features only available in those frameworks.

public class BlobCleanupTest
{
    [Test]
    public void CanCleanup()
    {
        // Arrange
        var blobServiceClient = Substitute.For<BlobServiceClient>();
        var blobCleanup = new BlobCleanup(blobServiceClient);
        
        // TODO: Mock BlobServiceClient
        
        // Act
        blobCleanup.Cleanup();
        
        // Assert
        
        // TODO: Verify deleted blob containers
    }
}

The test creates a mock of the BlobServiceClient class from the Azure.Storage.Blobs package. This is the main entry point for communicating with Azure Blob Storage. It then creates a new BlobCleanup, which is our class for deleting blob containers, and then calls the Cleanup method. The test contains two TODOs so let's go ahead and implement the first one. To make the code run, we need to mock the GetBlobContainers method. The method is virtual meaning that it can be easily mocked. But returning objects from the method isn't exactly straightforward. Let's write some code. I'll start by generating three container names and adding them to an array. One container for today, one for yesterday, and one for tomorrow:

var today = DateTime.Today;
var todayContainer = today.ToString("yyyy-MM-dd");
var tomorrowContainer = today.AddDays(1).ToString("yyyy-MM-dd");
var yesterdayContainer = today.AddDays(-1).ToString("yyyy-MM-dd");
var containers = new[] { todayContainer, tomorrowContainer, yesterdayContainer };

Next, we'd need for the GetBlobContainers method to return these three container names. The GetBlobContainers method doesn't return strings, though. The return type is of type Pageable<BlobContainerItem>. Neither Pageable or BlobContainerItem contains public constructors, so we need to use other options provided by the blob package:

var responsePage = Page<BlobContainerItem>.FromValues(
    containers
        .Select(item => BlobsModelFactory.BlobContainerItem(item, null))
        .ToArray(),
    continuationToken: null,
    response: null);

var pageable = Pageable<BlobContainerItem>.FromPages(new[] { responsePage });

blobServiceClient.GetBlobContainers().Returns(pageable);

Phew, that's quite a lot of lines to return what's basically a list of containers. Let's go through the lines one by one.

The Page class represents a single page of search results from blob storage. The API is based on paginated results meaning there's a maximum number of results returned as part of each response. To keep this example simple, I won't go into details about implementing or testing paginated results.

Would your users appreciate fewer errors?

➡️ Reduce errors by 90% with elmah.io error logging and uptime monitoring ⬅️

I use the static FromValues method to create an instance of the Page class since there's no public constructor. In the same way, BlobContainerItem instances are created by calling the static BlobContainerItem method on the BlobsModelFactory class. The method accepts the container name as the first parameter and a set of properties in the second parameter which we don't need for this implementation. The continuation token and response are not needed for the implementation or test why both are set to null.

With a single page of blob container items, we can use the FromPages method on the Pageable class. Finally, I use NSubstitute to return the pageable object from the GetBlobContainers method. With this setup in place, the Cleanup method will get our three test containers returned.

For the second and last TODO in the test, we need to verify that the Cleanup method deletes the containers from yesterday and tomorrow. We can do that using NSubstitute:

blobServiceClient.DidNotReceive().DeleteBlobContainer(todayContainer);
blobServiceClient.Received().DeleteBlobContainer(tomorrowContainer);
blobServiceClient.Received().DeleteBlobContainer(yesterdayContainer);

By using the DidNotReceive and Received methods, we make sure that the container with today's name is not deleted but the containers with yesterday's and tomorrow's names are deleted.

Here's the full test:

public class BlobCleanupTest
{
    [Test]
    public void CanCleanup()
    {
        // Arrange
        var blobServiceClient = Substitute.For<BlobServiceClient>();
        var blobCleanup = new BlobCleanup(blobServiceClient);

        var today = DateTime.Today;
        var todayContainer = today.ToString("yyyy-MM-dd");
        var tomorrowContainer = today.AddDays(1).ToString("yyyy-MM-dd");
        var yesterdayContainer = today.AddDays(-1).ToString("yyyy-MM-dd");
        var containers = new[] { todayContainer, tomorrowContainer, yesterdayContainer };

        var responsePage = Page<BlobContainerItem>.FromValues(
            containers
                .Select(item => BlobsModelFactory.BlobContainerItem(item, null))
                .ToArray(),
            continuationToken: null,
            response: null);

        var pageable = Pageable<BlobContainerItem>.FromPages(new[] { responsePage });

        blobServiceClient.GetBlobContainers().Returns(pageable);

        // Act
        blobCleanup.Cleanup();

        // Assert
        blobServiceClient.DidNotReceive().DeleteBlobContainer(todayContainer);
        blobServiceClient.Received().DeleteBlobContainer(tomorrowContainer);
        blobServiceClient.Received().DeleteBlobContainer(yesterdayContainer);
    }
}

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website