Search code examples
.netdockerdockerfiletestcontainers

Run dotnet tests in docker which use testcontainers


I have an integration tests project that executes as expected in VS. The integration tests use a MsSql testcontainer (from https://dotnet.testcontainers.org/).

My goal is to run these tests in an Azure DevOps pipeline within a docker image, as I do successfully for other projects which do not use testcontainers. For now I am just trying to run the tests within a docker image in my local machine. Unfortunately I am facing connection issues.

My environment:

  • .NET 6
  • OS: Windows
  • Docker Desktop with linux containers

My code:

Authentication.Api/MyProject.Authentication.Api/Dockerfile:

##########################################################
# build

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Authentication.Api/MyProject.Authentication.Api/MyProject.Authentication.Api.csproj", "Authentication.Api/MyProject.Authentication.Api/"]
COPY ["Authentication.Api/MyProject.Authentication.Api.IntegrationTests/MyProject.Authentication.Api.IntegrationTests.csproj", "Authentication.Api/MyProject.Authentication.Api.IntegrationTests/"]
RUN dotnet restore "Authentication.Api/MyProject.Authentication.Api/MyProject.Authentication.Api.csproj"
RUN dotnet restore "Authentication.Api/MyProject.Authentication.Api.IntegrationTests/MyProject.Authentication.Api.IntegrationTests.csproj"
COPY . .

WORKDIR "/src/Authentication.Api/MyProject.Authentication.Api"
RUN dotnet build "MyProject.Authentication.Api.csproj" -c Release -o /app/build

WORKDIR "/src/Authentication.Api/MyProject.Authentication.Api.IntegrationTests"
RUN dotnet build -c Release

##########################################################
# run test projects

FROM build AS tests
WORKDIR /src
VOLUME /var/run/docker.sock:/var/run/docker.sock
RUN dotnet test --no-build -c Release --results-directory /testresults --logger "trx;LogFileName=testresults_authentication_api_it.trx" /p:CollectCoverage=true /p:CoverletOutputFormat=json%2cCobertura /p:CoverletOutput=/testresults/coverage/ -p:MergeWith=/testresults/coverage/coverage.json  Authentication.Api/MyProject.Authentication.Api.IntegrationTests/MyProject.Authentication.Api.IntegrationTests.csproj

##########################################################
# create image

FROM build AS publish
WORKDIR "/src/Authentication.Api/MyProject.Authentication.Api"
RUN dotnet publish "MyProject.Authentication.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyProject.Authentication.Api.dll"]

Authentication.Api/MyProject.Authentication.Api.IntegrationTests/Factory/CustomWebApplicationFactory.cs:

public class CustomWebApplicationFactory<TProgram, TDbContext> : WebApplicationFactory<TProgram>, IAsyncLifetime, ICustomWebApplicationFactory
    where TProgram : class
    where TDbContext : DbContext
{
    private readonly MsSqlDatabaseProvider _applicationMsSqlDatabaseProvider;

    public CustomWebApplicationFactory()
    {
        _applicationMsSqlDatabaseProvider = new MsSqlDatabaseProvider();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
        => builder.ConfigureServices(services =>
        {
            services.Remove(services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)) ?? throw new InvalidOperationException());
            services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(_applicationMsSqlDatabaseProvider.Database.ConnectionString); });

            ServiceProvider? sp = services.BuildServiceProvider();
            using IServiceScope scope = sp.CreateScope();
            IServiceProvider scopedServices = scope.ServiceProvider;
            ILogger<CustomWebApplicationFactory<TProgram, TDbContext>> logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TProgram, TDbContext>>>();

            ApplicationDbContext applicationDbContext = scopedServices.GetRequiredService<ApplicationDbContext>();
            applicationDbContext.Database.EnsureCreated();
            logger.LogInformation("Ensured that the ApplicationDbContext DB is created.");
        });

    public async Task InitializeAsync() =>
        await _applicationMsSqlDatabaseProvider.Database.StartAsync();

    public new async Task DisposeAsync() =>
        await _applicationMsSqlDatabaseProvider.Database.DisposeAsync().AsTask();
}

{shared library path}/MsSqlDatabaseProvider.cs:

public class MsSqlDatabaseProvider
{
    private const string DbPassword = "my_dummy_password#123";
    private const string DbImage = "mcr.microsoft.com/mssql/server:2019-latest";

    public readonly TestcontainerDatabase Database;

    public MsSqlDatabaseProvider() =>
        Database = new TestcontainersBuilder<MsSqlTestcontainer>()
            .WithDatabase(new MsSqlTestcontainerConfiguration
            {
                Password = DbPassword,
            })
            .WithImage(DbImage)
            .WithCleanUp(true)
            .Build();
}

On command line I run docker build --progress=plain -f Authentication.Api\MyProject.Authentication.Api\Dockerfile --target tests --tag myproject-tests ..

And I am getting the following error:

Cannot detect the Docker endpoint. Use either the environment variables or the ~/.testcontainers.properties file to customize your configuration: https://dotnet.testcontainers.org/custom_configuration/ (Parameter 'DockerEndpointAuthConfig')

I tried adding the environment variable in docker, changing dockerfile to

RUN export DOCKER_HOST="tcp://192.168.99.100:2376" && dotnet test --no-build -c Release --results-directory /testresults --logger "trx;LogFileName=testresults_authentication_api_it.trx" /p:CollectCoverage=true /p:CoverletOutputFormat=json%2cCobertura /p:CoverletOutput=/testresults/coverage/ -p:MergeWith=/testresults/coverage/coverage.json  Authentication.Api/MyProject.Authentication.Api.IntegrationTests/MyProject.Authentication.Api.IntegrationTests.csproj

and adding .WithDockerEndpoint("tcp://192.168.99.100:2376") in MsSqlDatabaseProvider, but I ended up with another error:

System.Net.Http.HttpRequestException : Connection failed

System.Net.Sockets.SocketException : Connection refused

I do not know what value(s) I should use for docker host / docker endpoint. Or is the solution something else?

Thank you in advance!


Solution

  • I could manage to do it, with two major differences:

    1. The tests do not run on the docker image, but rather on the docker container.
    2. I am using docker compose now.

    docker-compose-tests.yml:

    version: '3.4'
    
    services:
      myproject.authentication.api.tests: # docker compose -f docker-compose-tests.yml up myproject.authentication.api.tests
        build:
          context: .
          dockerfile: Authentication.Api/MyProject.Authentication.Api/Dockerfile
          target: build
        command: >
            sh -cx "
                    dotnet test /src/Authentication.Api/MyProject.Authentication.Api.IntegrationTests/MyProject.Authentication.Api.IntegrationTests.csproj -c Release --results-directory /testresults --logger \"trx;LogFileName=testresults_authentication_api_it.trx\" /p:CollectCoverage=true /p:CoverletOutputFormat=json%2cCobertura /p:CoverletOutput=/testresults/coverage/ -p:MergeWith=/testresults/coverage/coverage.json"
        environment:
          - TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal # Needed in Docker Desktop (Windows), needs to be removed on linux hosts. Can be done with a override compose file.
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
          - coverage:/testresults/coverage
        container_name: myproject.authentication.api.tests
    

    ("sh" command is useful if more test projects are expected to run.)

    Authentication.Api/MyProject.Authentication.Api/Dockerfile:

    FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
    WORKDIR /src
    COPY ["Authentication.Api/MyProject.Authentication.Api/MyProject.Authentication.Api.csproj", "Authentication.Api/MyProject.Authentication.Api/"]
    COPY ["Authentication.Api/MyProject.Authentication.Api.IntegrationTests/MyProject.Authentication.Api.IntegrationTests.csproj", "Authentication.Api/MyProject.Authentication.Api.IntegrationTests/"]
    RUN dotnet restore "Authentication.Api/MyProject.Authentication.Api/MyProject.Authentication.Api.csproj"
    RUN dotnet restore "Authentication.Api/MyProject.Authentication.Api.IntegrationTests/MyProject.Authentication.Api.IntegrationTests.csproj"
    COPY . .
    
    WORKDIR "/src/Authentication.Api/MyProject.Authentication.Api"
    RUN dotnet build "MyProject.Authentication.Api.csproj" -c Release -o /app/build
    
    WORKDIR "/src/Authentication.Api/MyProject.Authentication.Api.IntegrationTests"
    RUN dotnet build -c Release
    

    Authentication.Api/MyProject.Authentication.Api.IntegrationTests/Factory/CustomWebApplicationFactory.cs: same as in the question.

    {shared library path}/MsSqlDatabaseProvider.cs:

    public class MsSqlDatabaseProvider
    {
        private const string DbImage = "mcr.microsoft.com/mssql/server:2019-latest";
        private const string DbUsername = "sa";
        private const string DbPassword = "my_dummy_password#123";
        private const ushort MssqlContainerPort = 1433;
    
    
        public readonly TestcontainerDatabase Database;
    
        public MsSqlDatabaseProvider() =>
            Database = new TestcontainersBuilder<MsSqlTestcontainer>()
                .WithDatabase(new MsSqlTestcontainerConfiguration
                {
                    Password = DbPassword,
                })
                .WithImage(DbImage)
                .WithCleanUp(true)
                .WithPortBinding(MssqlContainerPort, true)
                .WithEnvironment("ACCEPT_EULA", "Y")
                .WithEnvironment("MSSQL_SA_PASSWORD", DbPassword)
                .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("/opt/mssql-tools/bin/sqlcmd", "-S", $"localhost,{MssqlContainerPort}", "-U", DbUsername, "-P", DbPassword))
                .Build();
    }
    

    And I can run the tests in docker with docker compose -f docker-compose-tests.yml up myproject.authentication.api.tests.