Skip to content

Instantly share code, notes, and snippets.

@dj-nitehawk
Last active October 1, 2024 09:02
Show Gist options
  • Save dj-nitehawk/04a78cea10f2239eb81c958c52ec84e0 to your computer and use it in GitHub Desktop.
Save dj-nitehawk/04a78cea10f2239eb81c958c52ec84e0 to your computer and use it in GitHub Desktop.
Integration testing with TestContainers & AppFixture
using Testcontainers.MongoDb;
public class Sut : AppFixture<Program>
{
const string Database = "TestingDB";
const string RootUsername = "root";
const string RootPassword = "password";
MongoDbContainer _container = null!;
protected override async Task PreSetupAsync()
{
//this function is only ever called once before the WAF/SUT instance is created & cached.
//every derived AppFixture type will cache just one instance of a WAF/SUT.
//more info: https://fast-endpoints.com/docs/integration-unit-testing#app-fixture
_container = new MongoDbBuilder()
.WithImage("mongo")
.WithUsername(RootUsername)
.WithPassword(RootPassword)
.WithCommand("mongod")
.Build();
await _container.StartAsync();
}
protected override void ConfigureApp(IWebHostBuilder b)
{
b.ConfigureAppConfiguration(
c =>
{
c.AddInMemoryCollection(
new Dictionary<string, string?>
{
{ "Mongo:Host", _container.Hostname },
{ "Mongo:Port", _container.GetMappedPublicPort(27017).ToString() },
{ "Mongo:DbName", Database },
{ "Mongo:UserName", RootUsername },
{ "Mongo:Password", RootPassword }
});
});
}
protected override async Task TearDownAsync()
{
//await _container.DisposeAsync();
//NOTE: there's no need to dispose the container here as it will be automatically disposed by testcontainers pkg when the test run finishes.
// this is especially true if this AppFixture is used by many test-classes with WAF caching enabled.
// so, in general - containers don't need to be explicitly disposed, unless you disable WAF caching.
}
}
@dziedrius
Copy link

dziedrius commented Sep 19, 2024

@TheBenda from my testing it seems that PreSetupAsync is running once per assembly, SetupAsync runs once per test fixture, hence running migrations in SetupAsync can lead to slower tests and conflicts due to parallelization.
I did it like this:

public class Sut : AppFixture<Program>
{
    private PostgreSqlContainer? postgreSqlContainer;
    
    protected override async Task PreSetupAsync()
    {
        postgreSqlContainer = new PostgreSqlBuilder()
            .WithImage("postgres:16")
            .WithDatabase("db")
            .WithUsername("db")
            .WithPassword("db")
            .Build();

        await postgreSqlContainer.StartAsync();

        var connectionString = postgreSqlContainer.GetConnectionString();

        // this is optional, we're using environment variables for connection string
        Environment.SetEnvironmentVariable("DB_CONNECTION_STRING", connectionString);
             
        // in pre PreSetupAsync Services is not initialized yet,
        // hence need to create temporary service provider to run migrations
        // running migrations in SetupAsync is not viable as setup runs for each fixture
        // so if tests are running in parallel, migrations run in parallel too and that leads to complex situations
        var serviceCollection = new ServiceCollection();
        // custom extension method, so that the DbContext options would match test and actual program
        serviceCollection.AddMyDbContext();

        using (var serviceProvider = serviceCollection.BuildServiceProvider())
        {
            using (var dbContext = serviceProvider.GetRequiredService<MyDbContext>())
            {
                await dbContext.Database.MigrateAsync();
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment