testcontainers / testcontainers-dotnet

A library to support tests with throwaway instances of Docker containers for all compatible .NET Standard versions.
https://dotnet.testcontainers.org
MIT License
3.67k stars 254 forks source link

[Enhancement]: Support `ConfigurationProvider` (ASP.NET integration) together with modules #1068

Open HofmeisterAn opened 7 months ago

HofmeisterAn commented 7 months ago

Problem

There are several ways to integrate Testcontainers into ASP.NET (integration) tests. Developers often have to write the code to leverage Testcontainers into ASP.NET (integration) tests repeatedly.

Solution

To simplify the integration of dependent services into ASP.NET applications using Testcontainers, we can utilize Microsoft's IConfigurationSource interface and the ConfigurationProvider class. These allow us to initiate default module configurations and set up the actual ASP.NET application, making it straightforward for developers to incorporate Testcontainers into their ASP.NET integration tests and set up their ASP.NET configuration with the dependent services.

private sealed class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration(configure =>
        {
            configure.Add(new RedisConfigurationSource());
        });
    }
}

private sealed class RedisConfigurationSource : IConfigurationSource
{
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new RedisConfigurationProvider();
    }
}

private sealed class RedisConfigurationProvider : ConfigurationProvider
{
    private static readonly TaskFactory TaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

    public override void Load()
    {
        // Until the asynchronous configuration provider is available,
        // we use the TaskFactory to spin up a new task that handles the work:
        // https://github.com/dotnet/runtime/issues/79193
        // https://github.com/dotnet/runtime/issues/36018
        TaskFactory.StartNew(LoadAsync)
            .Unwrap()
            .ConfigureAwait(false)
            .GetAwaiter()
            .GetResult();
    }

    public async Task LoadAsync()
    {
        var redisContainer = new RedisBuilder().Build();

        await redisContainer.StartAsync()
            .ConfigureAwait(false);

        Set("ConnectionStrings:RedisCache", redisContainer.GetConnectionString());
    }
}

The interesting part here is the LoadAsync() member that starts the dependent service and sets the connection string. The actual app can simply read the connection string as it usually does using Configuration.GetConnectionString("RedisCache").

I am still considering the best place to implement and store the interface and class. Overall, I aim to avoid implementing them in every module and introducing extra dependencies like Microsoft.Extensions.Configuration to the modules.

Benefit

Developers will be able to integrate Testcontainers into tests much more simply. Leveraging it into ASP.NET (integration) tests would only require a single line and makes the startup and teardown implementation obsolete.

builder.ConfigureAppConfiguration(configure => configure.Add(new RedisConfigurationSource()));

Alternatives

-

Would you like to help contributing this enhancement?

Yes

asos-alexhaigh commented 1 month ago

I came up with the same idea and I've implemented a solution for the WireMock module I'd be happy to collaborate on making it a broader solution.

However, I am currently working on a version for the Azure CosmosDB module and I have found an issue I could see being a roadblock for other modules.

For the CosmosDB module you need to access _container.HttpClient when you're creating the CosmosClient for dependency injection otherwise you will be denied access. This means you need the initialized container accessible outside of the ConfigurationProvider after it has been started.

        var cosmosClient = new CosmosClient(
           // connection string for the emulator, this can be accessible by updating appsettings via "Set(key, value)"
            _container.GetConnectionString(), 
            new CosmosClientOptions
            {
                // The HTTP client the container provides automatically trusts the emulator's certificate. 
                // The emulator's SSL certificate is self signed and not trusted by default.
                // How can we make this HttpClient accessible? 
                HttpClientFactory = () => _container.HttpClient,
                ConnectionMode = ConnectionMode.Gateway,
            });

I imagine there are other modules where objects created directly from the container instance would be required for DI. Can you think of a solution to make them available to a WebApplicationFactory when setting up a test host?