I have an API that I would like to make integration tests for. I have a Program.cs and a custom made Startup.cs which is basically a static class containing extension methods to WebApplicationBuilder
to mimic the .NET behavior prior to .NET 6.
// Program.cs
WebApplication.CreateBuilder(args)
.ConfigureLogging()
.ConfigureServices()
.Build()
.Initialize()
.ConfigurePipeline()
.Run();
// Startup.cs
public static class Startup
{
public static WebApplicationBuilder ConfigureLogging(this WebApplicationBuilder builder)
{
// Configure logging
return builder;
}
public static WebApplicationBuilder ConfigureServices(this WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;
services.AddPooledDbContextFactory<AppDbContext>((serviceProvider, options) =>
{
var connectionString = configuration.GetConnectionString("Database");
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
dataSourceBuilder.EnableDynamicJson();
options.UseNpgsql(dataSourceBuilder.Build());
});
return builder;
}
public static WebApplication ConfigurePipeline(this WebApplication app)
{
// Register middleware
app.UseRouting();
app.MapControllers();
return app;
}
public static WebApplication Initialize(this WebApplication app)
{
// Resolve DbContext and call .Migrate()
return app;
}
}
What I am trying to do is create a custom WebApplicationFactory<>
and override some stuff in there, such as the database that will be used. I am using the Testcontainers.PostgreSql NuGet package and my factory looks like this:
public class MyFactory : WebApplicationFactory<IAssemblyMarker>, IAsyncLifetime
{
private readonly PostgreSqlContainer _postgreSqlContainer = new PostgreSqlBuilder()
.WithUsername("username")
.WithPassword("password")
.WithDatabase("mydb")
.Build();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
});
// If I use ConfigureTestServices, the test first calls ConfigureTestServices,
// then ConfigureServices, and then Startup.ConfigureServices (the extension method)
//builder.ConfigureTestServices(services =>
builder.ConfigureServices(services =>
{
var connectionString = _postgreSqlContainer.GetConnectionString();
services.RemoveAll<AppDbContext>();
services.RemoveAll<IDbContextFactory<AppDbContext>>();
services.RemoveAll<IDataTypeBuilder>();
services.AddSingleton<IDataTypeBuilder, NpgsqlDataTypeBuilder>();
services.AddPooledDbContextFactory<AppDbContext>(options =>
{
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString)
{
Name = "IntegrationTests App"
};
options.UseNpgsql(dataSourceBuilder.Build());
});
});
}
public async Task InitializeAsync()
{
await _postgreSqlContainer.StartAsync();
}
async Task IAsyncLifetime.DisposeAsync()
{
await _postgreSqlContainer.StopAsync();
}
}
The problem is, calling _myFactory.CreateClient()
will always throw an exception because the connection string in my appsettings.json
is defined for my local PostgreSQL container, which is naturally down for the integration tests. I did try setting breakpoints and I have a question: Why does my test method (after calling CreateClient) enter the WebApplicationFactory<>.ConfigureServices
(or ConfigureTestServices - I can't seem to find what the difference between the two is) and then the Startup.ConfigureServices
? What happens then is that the PostgreSqlContainer
connection string doesn't play a role in my tests at all, but the test methods try to connect to my database that is defined in my appsettings.json
.
What is the correct fix and workaround for this? Could the custom Startup class be the problem (since Startup.ConfigureServices
is a custom extension method)?
So, I managed to find a "workaround" (no idea if it should be done this way, though). I believe because my AppDbContext
is registered using AddPooledDbContextFactory<AppDbContext>
that I have not removed all service registrations using services.RemoveAll<>
in my WebApplicationFactory.ConfigureWebHost
inside builder.ConfigureTestServices
/ builder.ConfigureServices
(I still do not know what the difference between the two would be), so the AppDbContext
is registered in my custom Startup
class with pre-existing appsettings.json configuration and AFTER that, I try to re-register the DbContextFactory
in my MyFactory
.
So, my workaround so far is to remove ConfigureTestServices
and ConfigureServices
from my MyFactory : WebApplicationFactory
altogether and call ConfigureAppConfiguration
which allows me to remove any IConfiguration
sources and define my own Dictionary<>
which would have my custom (integration-test specific) values for the app configuration.
builder.ConfigureAppConfiguration((ctx, builder) =>
{
var configuration = new Dictionary<string, string?>
{
["ConnectionStrings:Database"] = _postgreSqlContainer.GetConnectionString(),
};
builder.Sources.Clear();
builder.AddInMemoryCollection(configuration);
});
The result of doing this is that when my Startup.ConfigureServices
is called by my Program
class, the IConfiguration
it uses has ONLY the key-value pairs that I have defined above, so the connection string that it tries to use is basically pre-set by my integration test MyFactory
.
I will mark this question as Answered. In case anyone has a better answer or explanation in the future, feel free to post them here.