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