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.65k stars 250 forks source link

[Enhancement]: Make the container name part of the reuse hash #1161

Closed 0xced closed 2 months ago

0xced commented 2 months ago

Problem

Since the reuse feature became available I started using Testcontainers to automate setting up a development database. I have created a hosted service that goes like this:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Testcontainers.MsSql;

namespace SampleCode;

internal class DockerDatabaseService : IHostedService
{
    private readonly IDbContextFactory<MyDbContext> _dbContextFactory;
    private readonly MsSqlContainer _container;

    public DockerDatabaseService(IHostEnvironment environment, IDbContextFactory<MyDbContext> dbContextFactory, ILoggerFactory loggerFactory)
    {
        if (!environment.IsDevelopment())
        {
            throw new NotSupportedException($"{nameof(DockerDatabaseService)} must only be used in the development environment but the current environment is \"{environment.EnvironmentName}\"");
        }

        _dbContextFactory = dbContextFactory;
        _container = new MsSqlBuilder()
            .WithLogger(loggerFactory.CreateLogger("Testcontainers"))
            .WithReuse(true)
            .WithName(environment.ApplicationName)
            .WithPortBinding(hostPort: 1433, containerPort: 1433)
            .Build();
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _container.StartAsync(cancellationToken);

        await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
        await dbContext.Database.MigrateAsync(cancellationToken);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await _container.StopAsync(cancellationToken);
    }
}

The Testcontainers.MsSql package is included conditionally for debug builds only:

<ItemGroup Condition="$(Configuration) == 'Debug'">
  <PackageReference Include="Testcontainers.MsSql" Version="3.8.0" />
</ItemGroup>
<ItemGroup Condition="$(Configuration) != 'Debug'">
  <Compile Remove="DockerDatabaseService.cs" />
</ItemGroup>

And the hosted service is also registered conditionally for debug builds in the development environment:

#if DEBUG
    if (environment.IsDevelopment())
    {
        services.AddHostedService<DockerDatabaseService>();
    }
#endif

It worked so well that I used it in several projects. But then the same container was reused across different projects. Since I explicitly set a container name (WithName(environment.ApplicationName)) I assumed that the reuse mechanism would use it but it does not.

Solution

I think the container name (just like network and volume names) should be part of the reuse hash.

Benefit

Container reuse would work out of the box by just setting an explicit container name.

Alternatives

Currently I'm working around this issue by configuring the container with a reuse-id label:

_container = new MsSqlBuilder()
    .WithLogger(loggerFactory.CreateLogger("Testcontainers"))
    .WithReuse(true)
    .WithLabel("reuse-id", environment.ApplicationName) // 👈 goes into the reuse hash
    .WithName(environment.ApplicationName)
    .WithPortBinding(hostPort: 1433, containerPort: 1433)
    .Build();

Would you like to help contributing this enhancement?

Yes: #1162