Integration Test in .NET using TestContainers

Integration tests aim to ensure that the integrated parts of the application(such as database and your app) work together as expected. They’re even more important if you are using Dapper, cause what if you made a mistake in your query? When you mock the database, you will not able to figure out the error.

We need 4 things to prepare integration tests using test containers in .NET:

  1. A custom WebApplicationFactory, to run our app and replace the dependencies. You can include mocks here if you want to.

 public sealed class CustomWebApplicationFactory : WebApplicationFactory<Program> 
 {
        private readonly MsSqlContainer _dbContainer;

        public CustomWebApplicationFactory(MsSqlTestcontainerFixture mssqlFixture)
        {
            _dbContainer = mssqlFixture.DbContainer;
        }

        protected override void ConfigureWebHost(IWebHostBuilder builder) 
        {
            builder.ConfigureServices(services =>
            {
                services.RemoveAll(typeof(IDataManager));
                services.AddScoped<IDataManager, DataManager>(serviceProvider =>
                {
                    var connectionString = _dbContainer.GetConnectionString();
                    return new DataManager(connectionString);
                });
            }
        }
 }
  1. Test container setting, to run our scripts and containers.

    public sealed class MsSqlTestcontainerFixture : IAsyncLifetime
    {
        public MsSqlContainer DbContainer { get; private set; } = default!;

        private const string Database = "SomeDatabase";
        public Task InitializeAsync()
        {
            DbContainer = new MsSqlBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithPortBinding(1401, 1433)
            .WithName(Database)
            .WithEnvironment("ACCEPT_EULA", "Y")
            .WithPassword("Ps12!Ext€RN@l")
            .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "SA", "-P", "Ps12!Ext€RN@l"))
            .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Data operations are done")) // To wait until our script is done, check the below 4th part
            .WithBindMount(Path.GetFullPath("Sql"), "/scripts/")
            .WithCommand("/bin/bash", "-c", "/scripts/db_init.sh")
            .WithCleanUp(true)
            .Build();
        }

        public Task DisposeAsync()
        {
            return DbContainer.DisposeAsync().AsTask(),
        }
    }
  1. Our test file

    public class MatchEventOutputSyncTests : IClassFixture<MsSqlTestcontainerFixture>
    {
        private readonly CustomWebApplicationFactory _factory;

        public MatchEventOutputSyncTests(MsSqlTestcontainerFixture msSqlTestcontainerFixture)
        {
            _factory = new CustomWebApplicationFactory(msSqlTestcontainerFixture);
        }

        [Fact]
        public async Task SomeTest() 
        {
            // Your tests
            using var client = _factory.CreateClient();
            var fixture = new Fixture();
            var requestModel = fixture.Build<SomeRequestModel>()
            .With(p => p.SomeId, 1)
            .Create();

            var result = await client.PostAsJsonAsync("someurl/operation", syncRequest);
            // FluentAssertions
            result.Should().NotBeNull();
            var returnValue = await result.Content.ReadAsStringAsync();
            var response = JsonConvert.DeserializeObject<SyncMatchEventResultModel>(returnValue);
            response.Should().NotBeNull();
        }
    }
  1. Your scripts(optional)

First db_init file

#!/usr/bin/env bash
set -m
./opt/mssql/bin/sqlservr & ./scripts/setup_database.sh
fg

Then setup_database file


#!/usr/bin/env bash
# Wait for database to startup 
for i in {1..60}; do
  ./opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P $SQLCMDPASSWORD  -Q "SELECT 1;" >> /dev/null 2>&1
  if [ $? -eq 0 ]; then
    echo "SQL Server is ready!"
    break
  fi
  echo "Waiting for SQL Server to start..."
  sleep 5
done

./opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P $SQLCMDPASSWORD -i ./scripts/db_setup.sql

# This one is being used for wait strategy
echo "Data operations are done" 

then in db_setup.sql, you can write your sql code to create tables and insert data.

However, there is one caveat for our scripts, if you are using Windows. Your scripts’ line endings are different than from Unix ones. What i do is, changing it from Notepad, from here.

Image

There you have it. After you run the tests, testcontainers will run the database in the container, then it will create the tables through your scripts, tests will run on a real database, when tests are done, the changes will not persist.