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)

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

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)

In our last post, 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. In this post, we will see how to perform integration tests for Azure Blob Storage using TestContainers and Azurite.

What is Azurite?

Azurite is an open-source emulator provides a free local environment for testing your Azure Blob, Queue Storage, and Table Storage applications. When you’re satisfied with how your application is working locally, switch to using an Azure Storage account in the cloud. The emulator provides cross-platform support on Windows, Linux, and macOS.

There are several different ways to install Azurite. You can install it as a Node.js package, as a Docker container, or as a standalone executable. In this post, we will use Azurite as a Docker container.

Why am I using Azurite instead of actual storage account?

IMO, it is always a good practice to use a local environment for testing. It will help you to avoid unnecessary costs and unnecessary headaches for managing the storage account. Also, it will help you to test your application in a more controlled environment.

What is TestContainers?

Testcontainers for .NET is a library to support tests with throwaway instances of Docker containers for all compatible .NET Standard versions. The library is built on top of the .NET Docker remote API and provides a lightweight implementation to support your test environment in all circumstances.

For example, When we will run our integration tests, you will see that the Azurite container will be created and started automatically.

And after the tests are completed, the container will be stopped and removed automatically.

Prerequisites

We will create a new project SystemUnderTest.IntegrationTest for integration tests. We will use the same project structure as in our last post. The only difference is list of NuGet packages we will use. Instead of Moq , we will add Testcontainers. Here is the code snippet of csproj for test project

SystemUnderTest.IntegrationTest.csproj
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
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <None Remove="appsettings.json" />
  </ItemGroup>

  <ItemGroup>
    <Content Include="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
	  <PackageReference Include="Testcontainers" Version="2.3.0" />
	  <PackageReference Include="xunit" Version="2.4.2" />
	  <PackageReference Include="Xunit.DependencyInjection" Version="8.7.0" />
	  <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="3.1.2">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\SystemUnderTest\SystemUnderTest.csproj" />
  </ItemGroup>

</Project>

Also, to make sure IAzBlobService is registered with DI, I have added following code in Startup.cs of SystemUnderTest.IntegrationTest project

Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SystemUnderTest.Interface;

namespace SystemUnderTest.IntegrationTest;

public class Startup
{
    public void ConfigureHost(IHostBuilder hostBuilder) =>
        hostBuilder
         .ConfigureServices((context, services) =>
         {
             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 are ready to write our first integration test. But before that, we need to understand couple of concepts of xUnit.

Problem Statement

In our case, we want to initialize a blob storage instance with a set of test data, and then leave that test data in place for use by multiple test classes or multiple test cases in the same class. We also want to clean up the blob storage after all the tests are executed.

This setup of test context typically happens in the Constructor of the test class and the cleanup happens in the Dispose method of the test class. But starting a Azurite container through TestContainers is expensive asynchronous operation and it is certainly not a good idea to do it in the synchronous constructor.

The correct way to do this in XUnit is through the IAsyncLifetime interface. This interface has two methods, InitializeAsync and DisposeAsync. The InitializeAsync method is called before the first test in the class is run, and the DisposeAsync method is called after the last test in the class is run. Below is the code snippet for Azurite container initialization and cleanup through IAsyncLifetime interface.

AzuriteContainer.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
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;

namespace SystemUnderTest.IntegrationTest;

public class AzuriteContainer : IAsyncLifetime
{
    private readonly AzuriteTestcontainer _azuriteContainer;
    private const string AZURE_IMAGE = "mcr.microsoft.com/azure-storage/azurite";
    private const int DEFAULT_BLOB_PORT = 10000;
    public AzuriteContainer()
    {
        _azuriteContainer = new TestcontainersBuilder<AzuriteTestcontainer>()
                            .WithAzurite(new AzuriteTestcontainerConfiguration(AZURE_IMAGE)
                            {
                                BlobServiceOnlyEnabled = true,
                            })
                            .WithPortBinding(DEFAULT_BLOB_PORT)
                            .Build();

    }
    public async Task DisposeAsync()
    {
        await _azuriteContainer.DisposeAsync();
    }

    public async Task InitializeAsync()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
        await _azuriteContainer.StartAsync(cts.Token);
    }
}

To setup the shared context below features can be used:

Feature When to use
Class Fixtures When you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished.
Collection Fixtures When you want to create a single test context and share it among tests in several test classes, and have it cleaned up after all the tests in the test classes have finished.

In our case, we want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished. So, we could use Class Fixture, but for demonstration purpose we will use Collection Fixture.However, Please see the steps for how to use -

Steps to use Class Fixture :

  • Create the fixture class, and put the startup code in the fixture class constructor.
  • If the fixture class needs to perform cleanup, implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.
  • Add IClassFixture<> to the test class.
  • If the test class needs access to the fixture instance, add it as a constructor argument, and it will be provided automatically.

Steps to use Collection Fixture :

  • Create the fixture class, and put the startup code in the fixture class constructor.
  • If the fixture class needs to perform cleanup, implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.
  • Create the collection definition class, decorating it with the [CollectionDefinition] attribute, giving it a unique name that will identify the test collection.
  • Add ICollectionFixture<> to the collection definition class.
  • Add the [Collection] attribute to all the test classes that will be part of the collection, using the unique name you provided to the test collection definition class’s [CollectionDefinition] attribute.
  • If the test classes need access to the fixture instance, add it as a constructor argument, and it will be provided automatically.

If you follow the table, this is exactly what I did for our Azurite container. Below is the code snippet for Collection Fixture.

AzuriteContainerInstanceCollectionFixture.cs
1
2
3
4
5
6
namespace SystemUnderTest.IntegrationTest;

[CollectionDefinition(nameof(AzuriteContainer))]
public class AzuriteContainerInstanceCollectionFixture : ICollectionFixture<AzuriteContainer>
{
}

Integration Test

Now we know some basic concepts, let’s write our first integration test. Below is the code snippet for the integration test.

AzBlobServiceTest.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using SystemUnderTest.Interface;

namespace SystemUnderTest.IntegrationTest;

[Collection(nameof(AzuriteContainer))]
public class IntegrationTests
{
    private readonly StringWriter Output = new();
    private readonly IAzBlobService _azBlobService;

    public IntegrationTests(IAzBlobService azBlobService, AzuriteContainer azuriteInstance)
    {
        Console.SetOut(Output);
       _azBlobService = azBlobService;
    }
    [Fact]
    public async Task File_Upload_Suceess()
    {
        await Program.UploadFileToAzBlobAsync(_azBlobService);
        Assert.Contains("File uploaded successfully", Output.ToString());
    }
}

Outro

In this article, we have learned how to write integration tests for Azure Storage using Azurite container using XUnit and Testcontainers. However there are many things I have skipped. For example, how to mock the Azure Storage SDK, run automated tests in your CI pipeline, etc. I will cover some of these topics in the next post.

You can find the source code here.