Docker Powered .NET Integration Tests With TestContainers

Last Update: June 24, 2024
.NET Integration Tests With TestContainers
Table of Contents
Contributors
Picture of Vivasoft Team
Vivasoft Team
Tech Stack
0 +
Want to accelerate your software development your company?

It has become a prerequisite for companies to develop custom software products to stay competitive.

Have you ever wondered how to smooth-running your .Net integration tests using Docker?

Imagine the efficiency of running your tests within isolated containers, ensuring consistent environments every time. This revolutionary approach is now possible with Docker Powered .Net Integration Tests using TestContainers.

Integration test confirms that the api call to business logic to database everything is working according to our needs. It is a test with real data.

Often people use in-memory databases. But an in-memory database can not provide all the functionality of a real database. That’s why we can’t claim it serves the purpose of a real database.

Solution?

Use the development database, that’s it.

Then why are you reading this blog? Let’s find it out…

Problem with the Development Database for Integration Tests

  • We have to clean up the database every time manually to get a fresh state.
  • That may cause development problems.
  • Time consuming, slows the testing process.
  • Unreliable test outcome
  • Variable environment testing & many more…

Solve these Problems with Test Containers

It spins up a database container while running the tests. When the tests are finished it will be automatically disposed of.

We will programmatically control the container creation and deletion. Once we are done with the setup we are free to write our tests without worrying about the database stuff.

  • Every time we will get a fresh database
  • So no need for cleaning
    No touching of the development database
  • We are providing a real database so tests are very reliable.
  • We can change the database image any time to provide a variable environment.

We will see a demo with .Net8 and xUnit. I have taken a simple .Net8 web api project and a xUnit test project. The api project simulates a simple relationship among company, employee and bonus

Packages Used in this Demo

I will focus on the xUnit test project. Here are the packages we need to install.

  • Microsoft.AspNetCore.Mvc.Testing
  • Testcontainers.MsSql
  • Shouldly

Microsoft.AspNetCore.Mvc.Testing package provides an in-memory test server to run our .Net api for testing, dependency injection support, setup host environment etc.

Testcontainers.MsSql will give us the docker container builder for Microsoft SqlServer database.

Shouldly is a nice assertion package.

What are the Prerequisites?

  • .Net 8 SDK https://dotnet.microsoft.com/en-us/download/dotnet/8.0
  • Docker (Install docker in linux or docker desktop for windows.)

Code Example:

				
					public class TestEnvironmentWebApplicationFactory 
    : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly MsSqlContainer _container = new MsSqlBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithPassword("Strongest_password_2024!")
            .Build();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            var dbContextServiceDescriptor = services.SingleOrDefault(s => 
                    s.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));

            if (dbContextServiceDescriptor is not null)
            {
                // Remove the actual dbcontext registration.
                services.Remove(dbContextServiceDescriptor);
            }

            // Register dbcontext with container connection string
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                string containerConnectionString = _container.GetConnectionString();
                options.UseSqlServer(containerConnectionString);
            });
        });
    }

    public async Task InitializeAsync()
    {
        await _container.StartAsync();
    }

    async Task IAsyncLifetime.DisposeAsync()
    {
        await _container.StopAsync();
    }
}

				
			

This class inherits the WebApplicationFactory<Program> which is provided by Microsoft.AspNetCore.Mvc.Testing package to create an in-memory test server of our api.

Here the Program class is from the Api project. Since we inherit it we can override the ConfigureWebHost method. While the application instance is started to build, all services will be registered from the Program class. When the app.Build() method is called, our custom WebApplicationFactory’s ConfigureWebHost method will be invoked.

So now we can remove the previous database registration and provide our container’s one. We can get the connection string from the container instance.

This class also implements the IAsyncLifetime interface provided by xUnit. Which gives us two important methods InitializeAsync and DisposeAsync. Using these two methods we can start and stop our container.

				
					public abstract class BaseIntegrationTest 
    : IClassFixture<TestEnvironmentWebApplicationFactory>, IDisposable
{
    public BaseIntegrationTest(TestEnvironmentWebApplicationFactory factory)
    {
        _serviceScope = factory.Services.CreateScope();
        _httpClient = factory.CreateClient();
        _httpClient.BaseAddress = new Uri("https://localhost:7294");
    }

    protected IServiceScope _serviceScope;
    protected HttpClient _httpClient;
    private JsonSerializerOptions _serializerOptions = new JsonSerializerOptions()
    {
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    public async Task<TData?> GetAsync<TData>(string url)
    {
        var result = await _httpClient.GetAsync(url);
        var content = await result.Content.ReadAsStringAsync();
        var data = JsonSerializer.Deserialize<TData>(content, _serializerOptions);

        return data;
    }

    public async Task<TResult?> PostAsync<TData, TResult>(string url, TData data)
    {
        var stringContent = new StringContent(JsonSerializer.Serialize(data), 
            Encoding.UTF8, "application/json");

        var result = await _httpClient.PostAsync(url, stringContent);
        var content = await result.Content.ReadAsStringAsync();
        var returnData = JsonSerializer.Deserialize<TResult>(content, _serializerOptions);

        return returnData;
    }

    public void Dispose()
    {
        _serviceScope.Dispose();
        _httpClient.Dispose();
    }
}

				
			

In the above section, we created a base class for our integration tests. It takes the TestEnvironmentWebApplicationFactory as IClassFixture. For each test xUnit creates a new instance of the test class. We can provide shared data among test cases using IClassFixture. The fixture is now available for dependency injection. With the factory, we create our HttpClient to do api calls. Here GetAsync & PostAsync are two utility methods for HttpClient.

Now we can test our CompaniesController. Here is our simple integration test class.

				
					public class CompaniesControllerTests : BaseIntegrationTest
    {
        public CompaniesControllerTests(TestEnvironmentWebApplicationFactory factory)
            : base(factory)
        {
        }

        [Fact]
        public async void GetCompanies_WhenCalled_ShouldReturnAllCompanies()
        {
            // Act
            // This request retrieves the companies created during migration.
            var companies = await GetAsync<List<CompanyResponse>>("/api/Companies");

            // Assert
            companies.ShouldNotBeNull();
            companies.Count().ShouldBe(2);
        }

        [Fact]
        public async void CreateCompany_GivenCompanyData_ShouldCreateCompany()
        {
            // Arrange
            var company = new CompanyCreateRequest
            {
                Name = "Autonemo",
                Address = "Bonani ahmed tower",
                Employees =
                [
                    new EmployeeCreateRequest
                    {
                        Name = "Aurpan",
                        Level = WebApi.Entities.Enums.Level.SDEL2,
                        Salary = 90000
                    },
                ],
                BonusList =
                [
                    new BonusCreateRequest
                    {
                        Title = "Performance",
                        Active = true,
                        AmountPercentage = 20,
                        Month = WebApi.Entities.Enums.Month.Aug,
                        EmployeeLevel = WebApi.Entities.Enums.Level.SDEL2
                    }
                ]
            };

            // Act
            var companyId = await PostAsync<CompanyCreateRequest, int>("/api/Companies", company);

            var createdCompany = await GetAsync<CompanyResponse>($"/api/Companies/{companyId}");

            // Assert
            createdCompany.ShouldNotBeNull();
            createdCompany.Name.ShouldBe(company.Name);
            createdCompany.Address.ShouldBe(company.Address);
            createdCompany.BonusList!.Count().ShouldBe(company.BonusList.Count());
            createdCompany.Employees!.Count().ShouldBe(company.Employees.Count());
        }

    }

				
			

How to run the test project?

Make sure your docker instance is running. For windows run the docker desktop app. If you clone the repository, and run the tests. First time it will take a long time to execute around 8 to 12 minutes depending on your internet connection.

When you run the tests for the first time, it will download the sql server official docker image. mcr.microsoft.com/mssql/server:2022-latest which is more than 1.5 GB.
So my recommendation is, download the image before running the test; with the command:

				
					docker pull http://mcr.microsoft.com/mssql/server:2022-latest
				
			

Now you are good to go. You can run the test with visual studio IDE’s test explorer. Also using the command. Open the repository in command line window and execute:

				
					dotnet test
				
			

When you run the tests, you will see two container spins up. One for ms sql server and another for the test container itself.

test container

When test running is finished, these containers will be gone immediately. That’s all, Happy testing…

References:

Make your .NET integration tests better by using Docker and TestContainers.

If you face any challenges implementing .NET Integretion, we’re here to help. Hire Dedicated .NET Developers for Your Project from Vivasoft.

Our skilled .NET developers at Vivasoft can set up these tools for you, providing reliable and efficient testing solutions.

Contact us today to discuss your testing needs and let us help you achieve high-quality results and faster development times.

Tech Stack
0 +
Accelerate Your Software Development Potential with Us
With our innovative solutions and dedicated expertise, success is a guaranteed outcome. Let's accelerate together towards your goals and beyond.
Blogs You May Love

Don’t let understaffing hold you back. Maximize your team’s performance and reach your business goals with the best IT Staff Augmentation