Hi Everyone!

This post is continuation of how to perform unit and integration tests for Azure Blob Storage using Azurite Test Containers, Moq and xUnit. Over the time, I will updated this page with links to individual posts :

Getting started with testing for Azure Blob Storage : Dependency Injection

Getting started with testing for Azure Blob Storage : Unit Test with help of Moq

Getting started with testing for Azure Blob Storage : Unit Test with help of FakeItEasy (Alternative to MoQ)

Getting started with testing for Azure Blob Storage : Integration Test with help of TestContainers and Azurite

This Post - Getting started with testing for Azure Blob Storage : Mocking Azure Blob/File Storage SDK

Getting started with testing for Azure Blob Storage : Mocking Azure Blob/File Storage SDK with help of FakeItEasy (Alternative to MoQ)

By far we have seen how to perform unit tests for Azure Blob Storage using Moq by hiding the dependency of Azure Storage SDK and actual implementation of it. But what if we want to test the actual implementation (i.e AzBlobService) of Azure Storage SDK? In this post, we will see how to perform unit tests for Azure Blob Storage using Moq by mocking the Azure Storage SDK.

At the end of our First post, We hid the dependency of Azure Storage SDK and actual implementation of it by creating an interface IAzBlobService and a class AzBlobService which implements the interface. What if file upload fails due to blank file name or file name with invalid characters? In this case, we want to test the actual implementation of AzBlobService and not the interface IAzBlobService. So, we need to find a way to test the actual implementation of AzBlobService.

Issue with Current Implementation

Currently AzBlobService is dependent only on IConfiguration and if you analyze further the below snippet, you will see that we are creating an instance of BlobServiceClient at line 13.

AzBlobService.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 public class AzBlobService : IAzBlobService
    {
        private readonly string azBlobConnectionString;
        private readonly string azBlobContainerName;
        public AzBlobService(IConfiguration configuration)
        {
            azBlobConnectionString = configuration.GetValue<string>("AzBlobSettings:ConnectionString")!;
            azBlobContainerName = configuration.GetValue<string>("AzBlobSettings:ContainerName")!;
        }
        public async Task<bool> UploadFileToAzBlobAsync(string fileName)
        {
            using FileStream stream = new(fileName, FileMode.Open);
            BlobContainerClient container = new(azBlobConnectionString, azBlobContainerName);

            try
            {
                await container.CreateIfNotExistsAsync();
                var client = container.GetBlobClient(fileName);
                await client.UploadAsync(stream);
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Unable to upload blob. Reason :{ex.Message}");
                return false;
            }
        }
    }

Subsequently, we are using CreateIfNotExistsAsync and UploadAsync methods of BlobContainerClient and BlobClient respectively. It means that our current implementation is tightly coupled with Azure Storage SDK.

Solution

To solve this problem first, we need to make sure our implementation is loosely coupled with Azure Storage SDK.

We will refractor our code further and remove the dependency of IConfiguration. But if remove the dependency of IConfiguration, how will we get the connection string and container name?

Let’s create a class AzBlobSettingsOption which will hold all the configuration details. and we will configure this class in Program.cs file as shown below.

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static IHostBuilder CreateHostBuilder(string[] args)
        {
          return Host
           .CreateDefaultBuilder(args)
           .ConfigureServices((context, services) =>
           {
               var azBlobSettingsOption = context.Configuration.GetSection(AzBlobSettingsOption.ConfigKey).Get<AzBlobSettingsOption>()!;

               services.AddSingleton(azBlobSettingsOption);
               services.AddSingleton<IAzBlobService, AzBlobService>();
           })
           .ConfigureAppConfiguration((context, config) =>
           {
               config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                     .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true);
           });
        }

Now, we can inject the configuration whereever we want to use.

Next, we will install the package Microsoft.Extensions.Azure which will help us to inject the depency of BlobServiceClient. I guess this package is one of the underrated package😒. Below is the code snippet of Program.cs

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host
           .CreateDefaultBuilder(args)
           .ConfigureServices((context, services) =>
           {
               var azBlobSettingsOption = context.Configuration.GetSection(AzBlobSettingsOption.ConfigKey).Get<AzBlobSettingsOption>()!;

               services.AddAzureClients(builder => builder
                                                    .AddBlobServiceClient(azBlobSettingsOption.ConnectionString)
                                                    .WithName(azBlobSettingsOption.ConnectionName));

               services.AddSingleton(azBlobSettingsOption);
               services.AddSingleton<IAzBlobService, AzBlobService>();
           })
           .ConfigureAppConfiguration((context, config) =>
           {
               config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                     .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true);
           });
        }

Now, Let’s refractor the code for AzBlobService which will now has a dependency of IAzureClientFactory and AzBlobSettingsOption instead of IConfiguration. Refractored version of AzBlobService shown below -

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class AzBlobService : IAzBlobService
    {
        private readonly BlobServiceClient _blobServiceClient;
        private readonly AzBlobSettingsOption _azBlobSettingsOption;
        public AzBlobService(IAzureClientFactory<BlobServiceClient> blobServiceClientFactory, AzBlobSettingsOption azBlobSettingsOption)
        {
            _azBlobSettingsOption = azBlobSettingsOption;
            _blobServiceClient = blobServiceClientFactory.CreateClient(_azBlobSettingsOption.ConnectionName);
        }
        public async Task<bool> UploadFileToAzBlobAsync(string fileName)
        {
            try
            {
                using FileStream stream = new(fileName, FileMode.Open);
                BlobContainerClient container = _blobServiceClient.GetBlobContainerClient(_azBlobSettingsOption.ContainerName);

                await container.CreateIfNotExistsAsync();
                var client = container.GetBlobClient(fileName);
                await client.UploadAsync(stream);
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Unable to upload blob. Reason :{ex.Message}");
                return false;
            }
        }
    }

This refractor will give us the flexibility to inject the mocked dependency of BlobServiceClient while writing unit tests.

Writing Unit Tests

Finally, we have reached to the point where we can write unit tests for AzBlobService. Let’s write the unit tests for AzBlobService using Moq and xUnit. For this, I have created seperate class named AzBlobServiceTest which will contain all the unit tests for AzBlobService in our unit test project.

AzBlobServiceTest.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class AzBlobServiceTest
{
    private readonly StringWriter Output = new();
    private const string _connectionName = "testconnection";

    public AzBlobServiceTest()
    {
        Console.SetOut(Output);
    }
    [Fact]
    public async Task File_Upload_Suceess()
    {
    // Arrange
        var _azBlobServiceClientFactory = new Mock<IAzureClientFactory<BlobServiceClient>>();
        var _azBlobServiceClient = new Mock<BlobServiceClient>();
        var _azBlobContainerClient = new Mock<BlobContainerClient>();
        var _azBlobClient = new Mock<BlobClient>();

        var blobContainerInfo = BlobsModelFactory.BlobContainerInfo(default,default);
        var blobContentInfo = BlobsModelFactory.BlobContentInfo(default,default,default,default,default);

        _azBlobContainerClient.Setup(x => x.CreateIfNotExistsAsync(default,default,default,default)).ReturnsAsync(Response.FromValue(blobContainerInfo,default!));
        _azBlobClient.Setup(x => x.UploadAsync(It.IsAny<Stream>(), default, default, default, default,default,default,default)).ReturnsAsync(Response.FromValue(blobContentInfo,default!));
        _azBlobContainerClient.Setup(x => x.GetBlobClient(It.IsAny<string>())).Returns(_azBlobClient.Object);
        _azBlobServiceClient.Setup(x => x.GetBlobContainerClient(It.IsAny<string>())).Returns(_azBlobContainerClient.Object);

        _azBlobServiceClientFactory.Setup(x => x.CreateClient(_connectionName)).Returns(_azBlobServiceClient.Object);
        var _azBlobSettingsOption = new AzBlobSettingsOption()
        {
            ConnectionName = _connectionName,
        };
        var _sut = new AzBlobService(_azBlobServiceClientFactory.Object, _azBlobSettingsOption);

    // Act
        var result = await _sut.UploadFileToAzBlobAsync("samplefile.txt");

    // Assert
        Assert.True(result);
    }

     [Fact]
    public async Task File_Upload_Fail()
    {
    // Arrange
        var _azBlobServiceClientFactory = new Mock<IAzureClientFactory<BlobServiceClient>>();
        var _azBlobServiceClient = new Mock<BlobServiceClient>();
        var _azBlobContainerClient = new Mock<BlobContainerClient>();
        var _azBlobClient = new Mock<BlobClient>();

        var blobContainerInfo = BlobsModelFactory.BlobContainerInfo(default,default);
        var blobContentInfo = BlobsModelFactory.BlobContentInfo(default,default,default,default,default);

        _azBlobContainerClient.Setup(x => x.CreateIfNotExistsAsync(default,default,default,default)).ReturnsAsync(Response.FromValue(blobContainerInfo,default!));
        _azBlobClient.Setup(x => x.UploadAsync(It.IsAny<Stream>(), default, default, default, default,default,default,default)).ReturnsAsync(Response.FromValue(blobContentInfo,default!));
        _azBlobContainerClient.Setup(x => x.GetBlobClient(It.IsAny<string>())).Returns(_azBlobClient.Object);
        _azBlobServiceClient.Setup(x => x.GetBlobContainerClient(It.IsAny<string>())).Returns(_azBlobContainerClient.Object);

        _azBlobServiceClientFactory.Setup(x => x.CreateClient(_connectionName)).Returns(_azBlobServiceClient.Object);
        var _azBlobSettingsOption = new AzBlobSettingsOption()
        {
            ConnectionName = _connectionName,
        };
        var _sut = new AzBlobService(_azBlobServiceClientFactory.Object, _azBlobSettingsOption);

    // Act
        var result = await _sut.UploadFileToAzBlobAsync("");

    // Assert
        Assert.False(result);
        Assert.Contains("Unable to upload blob. Reason", Output.ToString());
    }
}
Nothing fancy here, we are just mocking the dependency of BlobServiceClient and BlobContainerClient and injecting it to AzBlobService using AzBlobServiceClientFactory. Also we tried to mock all the required methods of BlobServiceClient and BlobContainerClient which are used in AzBlobService.

However, I would like to bring your attention to the line 19 and 20 of above code snippet, where we are using BlobsModelFactory to create the instance of BlobContainerInfo and BlobContentInfo.

This factory class is very powerful and it can be used to create the instance of any class which is part of Azure.Storage.Blobs namespace.

I have also created the scenario where file upload fails due to blank file name to show how to write negative unit tests.

Conclusion

In this article, we have learned how to write loosely coupled code for Azure Storage SDK and how to mock the Azure Storage SDK using Moq and xUnit. This whole series of articles was focused on Azure Blob Storage but you can use the same approach for Azure File Storage as well. For Azure File Storage, you just need to replace the BlobServiceClient with FileServiceClient and BlobContainerClient with ShareClient and BlobClient with ShareFileClient, and you are good to go. Also to mock the respose you will have FilesModelFactory instead of BlobsModelFactory.

You can find the source code here.