简体   繁体   中英

ASP.NET Core Integration tests with dotnet-testcontainers

I am trying to add some integration tests for a as.netcore v6 webapi following the docs - https://learn.microsoft.com/en-us/as.net/core/test/integration-tests?view=as.netcore-6.0#as.net-core-integration-tests .

My webapi database is SQLServer. I want the tests to be run against an actual SQLServer db and not in-memory database. I came across do.net-testcontainers - https://github.com/HofmeisterAn/do.net-testcontainers and thinking of using this so I do not need to worry about the resetting the db as the container is removed once test is run.

So this is what I plan to do:

  1. Start-up a SQLServer testcontainer before the test web host is started. In this case, the test web host is started using WebApplicationFactory. So the started wen host has a db to connect with. Otherwise the service start will fail.
  2. Run the test. The test would add some test data before its run.
  3. Then remove the SQLServer test container along with the Disposing of test web host.

This way the I can start the test web host that connects to a clean db running in a container, run the tests.

Does this approach sound right? OR Has someone used do.net-testcontainers to spin up a container for their application tests and what approach worked.

I wrote about this approach here .

You basically need to create a custom WebApplicationFactory and replace the connection string in your database context with the one pointing to your test container.

Here is an example, that only requires slight adjustments to match the MSSQL docker image.

public class IntegrationTestFactory<TProgram, TDbContext> : WebApplicationFactory<TProgram>, IAsyncLifetime
    where TProgram : class where TDbContext : DbContext
{
    private readonly TestcontainerDatabase _container;

    public IntegrationTestFactory()
    {
        _container = new TestcontainersBuilder<PostgreSqlTestcontainer>()
            .WithDatabase(new PostgreSqlTestcontainerConfiguration
            {
                Database = "test_db",
                Username = "postgres",
                Password = "postgres",
            })
            .WithImage("postgres:11")
            .WithCleanUp(true)
            .Build();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.RemoveProdAppDbContext<TDbContext>();
            services.AddDbContext<TDbContext>(options => { options.UseNpgsql(_container.ConnectionString); });
            services.EnsureDbCreated<TDbContext>();
        });
    }

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

    public new async Task DisposeAsync() => await _container.DisposeAsync();
}

And here are the extension methods to replace and initialize your database context.

public static class ServiceCollectionExtensions
{
    public static void RemoveDbContext<T>(this IServiceCollection services) where T : DbContext
    {
        var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<T>));
        if (descriptor != null) services.Remove(descriptor);
    }

    public static void EnsureDbCreated<T>(this IServiceCollection services) where T : DbContext
    {
        var serviceProvider = services.BuildServiceProvider();

        using var scope = serviceProvider.CreateScope();
        var scopedServices = scope.ServiceProvider;
        var context = scopedServices.GetRequiredService<T>();
        context.Database.EnsureCreated();
    }
}

There are another two ways to leverage Testcontainers for .NET in-process into your ASP.NET application and even a third way out-of-process without any dependencies to the application.

1. Using .NET's configuration providers

A very simple in-process setup passes the database connection string using the environment variable configuration provider to the application. You do not need to mess around with the WebApplicationFactory . All you need to do is set the configuration before creating the WebApplicationFactory instance in your tests.

The example below passes the HTTPS configuration incl. the database connection string of a Microsoft SQL Server instance spun up by Testcontainers to the application.

Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "https://+");
Environment.SetEnvironmentVariable("ASPNETCORE_Kestrel__Certificates__Default__Path", "certificate.crt");
Environment.SetEnvironmentVariable("ASPNETCORE_Kestrel__Certificates__Default__Password", "password");
Environment.SetEnvironmentVariable("ConnectionStrings__DefaultConnection", _mssqlContainer.ConnectionString);
_webApplicationFactory = new WebApplicationFactory<Program>();
_serviceScope = _webApplicationFactory.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
_httpClient = _webApplicationFactory.CreateClient();

This example follows the mentioned approach above.

2. Using .NET's hosted service

A more advanced approach spins up the dependent database and seeds it during the application start. It not just helps writing better integration tests, it integrates well into daily development and significantly improves the development experience and productivity.

Spin up the dependent container by implementing IHostedService :

public sealed class DatabaseContainer : IHostedService
{
  private readonly TestcontainerDatabase _container = new TestcontainersBuilder<MsSqlTestcontainer>()
    .WithDatabase(new DatabaseContainerConfiguration())
    .Build();

  public Task StartAsync(CancellationToken cancellationToken)
  {
    return _container.StartAsync(cancellationToken);
  }

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

  public string GetConnectionString()
  {
    return _container.ConnectionString;
  }
}

Add the hosted service to your application builder configuration:

builder.Services.AddSingleton<DatabaseContainer>();
builder.Services.AddHostedService(services => services.GetRequiredService<DatabaseContainer>());

Resolve the hosted service and pass the connection string to your database context:

builder.Services.AddDbContext<MyDbContext>((services, options) =>
{
  var databaseContainer = services.GetRequiredService<DatabaseContainer>();
  options.UseSqlServer(databaseContainer.GetConnectionString());
});

This example uses .NET's hosted service to leverage Testcontainers into the application start. By overriding the database context's OnModelCreating(ModelBuilder) , this approach even takes care of creating the database schema and seeding data via Entity Framework while developing.

3. Running inside a container

In some use cases, it might be necessary or a good approach to run the application out-of-process and inside a container. This increases the level of abstractions and removes the direct dependencies to the application. The services are only available through their public API (eg HTTP(S) endpoint).

The configuration follows the same approach as 1 . Use environment variables to configure the application running inside a container. Testcontainers builds the necessary container image and takes care of the container lifecycle.

_container = new TestcontainersBuilder<TestcontainersContainer>()
  .WithImage(Image)
  .WithNetwork(_network)
  .WithPortBinding(HttpsPort, true)
  .WithEnvironment("ASPNETCORE_URLS", "https://+")
  .WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Path", _certificateFilePath)
  .WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Password", _certificatePassword)
  .WithEnvironment("ConnectionStrings__DefaultConnection", _connectionString)
  .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(HttpsPort))
  .Build();

This example sets up all necessary Docker resources to spin up a throwaway out-of-process test environment.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM